diff --git a/.eslintignore b/.eslintignore
index da39d3ccf..c37217709 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -23,5 +23,6 @@ plugins/*
!plugins/talk-plugin-member-since
!plugins/talk-plugin-ignore-user
!plugins/talk-plugin-moderation-actions
+!plugins/talk-plugin-toxic-comments
node_modules
diff --git a/.eslintrc.json b/.eslintrc.json
index 237650932..8ca153cbc 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -3,7 +3,9 @@
"es6": true,
"node": true
},
- "extends": "eslint:recommended",
+ "extends": [
+ "eslint:recommended"
+ ],
"parserOptions": {
"ecmaVersion": 2017
},
@@ -12,9 +14,7 @@
"json"
],
"rules": {
- "indent": ["error",
- 2
- ],
+ "indent": ["error", 2],
"no-console": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single"],
@@ -29,7 +29,7 @@
"no-global-assign": "error",
"no-implied-eval": "error",
"lines-around-comment": ["warn", {"beforeLineComment": true}],
- "spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }],
+ "spaced-comment": ["warn", "always", {"line": {"exceptions": ["-", "="]}}],
"no-script-url": "error",
"no-throw-literal": "error",
"yoda": "warn",
@@ -41,32 +41,20 @@
"object-curly-spacing": "warn",
"space-infix-ops": ["error"],
"space-in-parens": ["error", "never"],
- "space-unary-ops": ["error", {
- "words": true,
- "nonwords": false
- }],
+ "space-unary-ops": ["error", {"words": true, "nonwords": false}],
"no-const-assign": "error",
"no-duplicate-imports": "error",
"prefer-template": "warn",
- "comma-spacing": ["error", {
- "after": true
- }],
+ "comma-spacing": ["error", {"after": true}],
"no-var": "error",
"no-lonely-if": "error",
"curly": "error",
- "no-unused-vars": ["error", {
- "argsIgnorePattern": "^_|next",
- "varsIgnorePattern": "^_"
- }],
- "no-multiple-empty-lines": ["error", {
- "max": 1
- }],
- "newline-per-chained-call": ["error", {
- "ignoreChainWithDepth": 2
- }],
+ "no-unused-vars": ["error", {"argsIgnorePattern": "^_|next", "varsIgnorePattern": "^_"}],
+ "no-multiple-empty-lines": ["error", {"max": 1}],
+ "newline-per-chained-call": ["error", {"ignoreChainWithDepth": 2}],
"promise/no-return-wrap": "error",
"promise/param-names": "error",
- "promise/catch-or-return": "error",
+ "promise/catch-or-return": "warn",
"promise/no-native": "off",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
diff --git a/.gitignore b/.gitignore
index 672409dbf..58683aec1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,7 @@ plugins/*
!plugins/talk-plugin-comment-content
!plugins/talk-plugin-permalink
!plugins/talk-plugin-featured-comments
+!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-sort-newest
!plugins/talk-plugin-sort-oldest
!plugins/talk-plugin-sort-most-replied
@@ -39,5 +40,6 @@ plugins/*
!plugins/talk-plugin-member-since
!plugins/talk-plugin-ignore-user
!plugins/talk-plugin-moderation-actions
+!plugins/talk-plugin-toxic-comments
**/node_modules/*
diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js
index dcd660c25..d50aeb3f6 100644
--- a/client/coral-admin/src/AppRouter.js
+++ b/client/coral-admin/src/AppRouter.js
@@ -1,6 +1,6 @@
import React from 'react';
-import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
import PropTypes from 'prop-types';
+import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
import Configure from 'routes/Configure';
import Dashboard from 'routes/Dashboard';
diff --git a/client/coral-admin/src/actions/install.js b/client/coral-admin/src/actions/install.js
index d6b27115a..02d562d65 100644
--- a/client/coral-admin/src/actions/install.js
+++ b/client/coral-admin/src/actions/install.js
@@ -128,18 +128,18 @@ const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error});
-export const checkInstall = (next) => (dispatch, _, {rest}) => {
+export const checkInstall = (next) => async (dispatch, _, {rest}) => {
dispatch(checkInstallRequest());
- rest('/setup')
- .then(({installed}) => {
- dispatch(checkInstallSuccess(installed));
- if (installed) {
- next();
- }
- })
- .catch((error) => {
- console.error(error);
- const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
- dispatch(checkInstallFailure(errorMessage));
- });
+
+ try {
+ const {installed} = await rest('/setup');
+ dispatch(checkInstallSuccess(installed));
+ if (installed) {
+ next();
+ }
+ } catch (error) {
+ console.error(error);
+ const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
+ dispatch(checkInstallFailure(errorMessage));
+ }
};
diff --git a/client/coral-admin/src/components/ActionButton.js b/client/coral-admin/src/components/ActionButton.js
index 3dbac206b..b8778bef0 100644
--- a/client/coral-admin/src/components/ActionButton.js
+++ b/client/coral-admin/src/components/ActionButton.js
@@ -1,4 +1,6 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
+
import styles from './ModerationList.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../utils/moderationQueueActionsMap';
diff --git a/client/coral-admin/src/components/ActionsMenu.js b/client/coral-admin/src/components/ActionsMenu.js
index ff4a9c5b3..6e236bf1c 100644
--- a/client/coral-admin/src/components/ActionsMenu.js
+++ b/client/coral-admin/src/components/ActionsMenu.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Button, Icon} from 'coral-ui';
import {Menu} from 'react-mdl';
import cn from 'classnames';
diff --git a/client/coral-admin/src/components/AdminLogin.js b/client/coral-admin/src/components/AdminLogin.js
index 014773a1b..5f9712127 100644
--- a/client/coral-admin/src/components/AdminLogin.js
+++ b/client/coral-admin/src/components/AdminLogin.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import Layout from 'coral-admin/src/components/ui/Layout';
import styles from './NotFound.css';
import {Button, TextField, Alert, Success} from 'coral-ui';
diff --git a/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/components/BanUserDialog.js
index bade3578d..169afe043 100644
--- a/client/coral-admin/src/components/BanUserDialog.js
+++ b/client/coral-admin/src/components/BanUserDialog.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import styles from './BanUserDialog.css';
diff --git a/client/coral-admin/src/components/CommentType.js b/client/coral-admin/src/components/CommentType.js
index 075ec5a78..4d1ff7e67 100644
--- a/client/coral-admin/src/components/CommentType.js
+++ b/client/coral-admin/src/components/CommentType.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './CommentType.css';
import {Icon} from 'coral-ui';
import cn from 'classnames';
diff --git a/client/coral-admin/src/components/EmptyCard.js b/client/coral-admin/src/components/EmptyCard.js
index ae291bcad..8adc63747 100644
--- a/client/coral-admin/src/components/EmptyCard.js
+++ b/client/coral-admin/src/components/EmptyCard.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Card} from 'coral-ui';
const EmptyCard = (props) => (
diff --git a/client/coral-admin/src/components/FlagBox.js b/client/coral-admin/src/components/FlagBox.js
index 240d52078..05fd14128 100644
--- a/client/coral-admin/src/components/FlagBox.js
+++ b/client/coral-admin/src/components/FlagBox.js
@@ -1,4 +1,5 @@
-import React, {Component, PropTypes} from 'react';
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
import {Icon} from 'coral-ui';
import styles from './FlagBox.css';
import t from 'coral-framework/services/i18n';
diff --git a/client/coral-admin/src/components/LoadMore.js b/client/coral-admin/src/components/LoadMore.js
index 969c6734f..eaacb4413 100644
--- a/client/coral-admin/src/components/LoadMore.js
+++ b/client/coral-admin/src/components/LoadMore.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import styles from './LoadMore.css';
import cn from 'classnames';
diff --git a/client/coral-admin/src/components/ModerationKeysModal.js b/client/coral-admin/src/components/ModerationKeysModal.js
index c4f3f1e1d..abdffa2b1 100644
--- a/client/coral-admin/src/components/ModerationKeysModal.js
+++ b/client/coral-admin/src/components/ModerationKeysModal.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import Modal from 'components/Modal';
import styles from './ModerationKeysModal.css';
import t from 'coral-framework/services/i18n';
diff --git a/client/coral-admin/src/components/ModerationList.js b/client/coral-admin/src/components/ModerationList.js
index e2b536f2c..adfe63051 100644
--- a/client/coral-admin/src/components/ModerationList.js
+++ b/client/coral-admin/src/components/ModerationList.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './ModerationList.css';
import key from 'keymaster';
import Hammer from 'hammerjs';
diff --git a/client/coral-admin/src/components/SuspendUserDialog.js b/client/coral-admin/src/components/SuspendUserDialog.js
index 0ed947506..d56201e2f 100644
--- a/client/coral-admin/src/components/SuspendUserDialog.js
+++ b/client/coral-admin/src/components/SuspendUserDialog.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import {RadioGroup, Radio} from 'react-mdl';
import styles from './SuspendUserDialog.css';
diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js
index a7eadbac3..fd44d5f17 100644
--- a/client/coral-admin/src/components/UserDetail.js
+++ b/client/coral-admin/src/components/UserDetail.js
@@ -28,16 +28,26 @@ export default class UserDetail extends React.Component {
bulkReject: PropTypes.func.isRequired,
}
- rejectThenReload = (info) => {
- this.props.rejectComment(info).then(() => {
+ rejectThenReload = async (info) => {
+ try {
+ await this.props.rejectComment(info);
this.props.data.refetch();
- });
+ } catch (err) {
+
+ // TODO: handle error.
+ console.error(err);
+ }
}
- acceptThenReload = (info) => {
- this.props.acceptComment(info).then(() => {
+ acceptThenReload = async (info) => {
+ try {
+ await this.props.acceptComment(info);
this.props.data.refetch();
- });
+ } catch (err) {
+
+ // TODO: handle error.
+ console.error(err);
+ }
}
showAll = () => {
@@ -133,7 +143,7 @@ export default class UserDetail extends React.Component {
diff --git a/client/coral-admin/src/components/UserDetailComment.js b/client/coral-admin/src/components/UserDetailComment.js
index 4e31d338b..fa196dc41 100644
--- a/client/coral-admin/src/components/UserDetailComment.js
+++ b/client/coral-admin/src/components/UserDetailComment.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Link} from 'react-router';
import {Icon} from 'coral-ui';
diff --git a/client/coral-admin/src/components/ui/Drawer.js b/client/coral-admin/src/components/ui/Drawer.js
index b49caf30a..2b728a33a 100644
--- a/client/coral-admin/src/components/ui/Drawer.js
+++ b/client/coral-admin/src/components/ui/Drawer.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Navigation, Drawer} from 'react-mdl';
import {IndexLink, Link} from 'react-router';
import styles from './Drawer.css';
diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js
index b43fa8b35..560935c2a 100644
--- a/client/coral-admin/src/components/ui/Header.js
+++ b/client/coral-admin/src/components/ui/Header.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
import {Link, IndexLink} from 'react-router';
import styles from './Header.css';
@@ -87,8 +88,8 @@ const CoralHeader = ({
-
- {`v${process.env.VERSION}`}
+
+ {`v${process.env.VERSION}`}
diff --git a/client/coral-admin/src/components/ui/Layout.js b/client/coral-admin/src/components/ui/Layout.js
index 10433048e..3aa056d9e 100644
--- a/client/coral-admin/src/components/ui/Layout.js
+++ b/client/coral-admin/src/components/ui/Layout.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Layout as LayoutMDL} from 'react-mdl';
import Header from './Header';
import Drawer from './Drawer';
diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js
index 0eb81a9a1..5effb2b84 100644
--- a/client/coral-admin/src/containers/UserDetail.js
+++ b/client/coral-admin/src/containers/UserDetail.js
@@ -36,14 +36,19 @@ class UserDetailContainer extends React.Component {
isLoadingMore = false;
// status can be 'ACCEPTED' or 'REJECTED'
- bulkSetCommentStatus = (status) => {
+ bulkSetCommentStatus = async (status) => {
const changes = this.props.selectedCommentIds.map((commentId) => {
return this.props.setCommentStatus({commentId, status});
});
- Promise.all(changes).then(() => {
+ try {
+ await Promise.all(changes);
this.props.clearUserDetailSelections(); // un-select everything
- });
+ } catch (err) {
+
+ // TODO: handle error.
+ console.error(err);
+ }
}
bulkReject = () => {
diff --git a/client/coral-admin/src/routes/Community/components/FlaggedUser.js b/client/coral-admin/src/routes/Community/components/FlaggedUser.js
index 5b2238592..81d7790fe 100644
--- a/client/coral-admin/src/routes/Community/components/FlaggedUser.js
+++ b/client/coral-admin/src/routes/Community/components/FlaggedUser.js
@@ -85,7 +85,7 @@ class User extends React.Component {
{t('community.flags')}({ user.actions.length })
:
- { user.action_summaries.map(
+ { user.action_summaries.map(
(action, i) => {
return
{shortReasons[action.reason]} ({action.count})
diff --git a/client/coral-admin/src/routes/Community/components/RejectUsernameDialog.js b/client/coral-admin/src/routes/Community/components/RejectUsernameDialog.js
index 128946b61..a9877368a 100644
--- a/client/coral-admin/src/routes/Community/components/RejectUsernameDialog.js
+++ b/client/coral-admin/src/routes/Community/components/RejectUsernameDialog.js
@@ -1,4 +1,5 @@
-import React, {Component, PropTypes} from 'react';
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
import {Dialog, Button} from 'coral-ui';
import styles from './RejectUsernameDialog.css';
@@ -48,11 +49,15 @@ class RejectUsernameDialog extends Component {
const cancel = this.props.handleClose;
const next = () => this.setState({stage: stage + 1});
- const suspend = () => {
- rejectUsername({id: user.user.id, message: this.state.email})
- .then(() => {
- this.props.handleClose();
- });
+ const suspend = async () => {
+ try {
+ await rejectUsername({id: user.user.id, message: this.state.email});
+ this.props.handleClose();
+ } catch (err) {
+
+ // TODO: handle error.
+ console.error(err);
+ }
};
const suspendModalActions = [
diff --git a/client/coral-admin/src/routes/Configure/components/ModerationSettings.js b/client/coral-admin/src/routes/Configure/components/ModerationSettings.js
index 99f3435e2..92a5413e2 100644
--- a/client/coral-admin/src/routes/Configure/components/ModerationSettings.js
+++ b/client/coral-admin/src/routes/Configure/components/ModerationSettings.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './Configure.css';
import {Card} from 'coral-ui';
import {Checkbox} from 'react-mdl';
diff --git a/client/coral-admin/src/routes/Configure/components/TechSettings.js b/client/coral-admin/src/routes/Configure/components/TechSettings.js
index 2dca7b207..4f1202a67 100644
--- a/client/coral-admin/src/routes/Configure/components/TechSettings.js
+++ b/client/coral-admin/src/routes/Configure/components/TechSettings.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Card} from 'coral-ui';
import Domainlist from './Domainlist';
import EmbedLink from './EmbedLink';
diff --git a/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js b/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js
index 5b43a605c..3f8af141b 100644
--- a/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js
+++ b/client/coral-admin/src/routes/Dashboard/components/ActivityWidget.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Link} from 'react-router';
import styles from './Widget.css';
import t from 'coral-framework/services/i18n';
diff --git a/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js b/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js
index 6864acb6b..a2e6ea9f1 100644
--- a/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js
+++ b/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './Dashboard.css';
import {Icon} from 'coral-ui';
diff --git a/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js b/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js
index 5dfbfb3ac..2f3548cf1 100644
--- a/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js
+++ b/client/coral-admin/src/routes/Dashboard/components/FlagWidget.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Link} from 'react-router';
import styles from './Widget.css';
diff --git a/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js b/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js
index aeb31c318..7525f7223 100644
--- a/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js
+++ b/client/coral-admin/src/routes/Dashboard/components/LikeWidget.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Link} from 'react-router';
import styles from './Widget.css';
import t from 'coral-framework/services/i18n';
diff --git a/client/coral-admin/src/routes/Install/containers/Install.js b/client/coral-admin/src/routes/Install/containers/Install.js
index 442127e47..77486a9c1 100644
--- a/client/coral-admin/src/routes/Install/containers/Install.js
+++ b/client/coral-admin/src/routes/Install/containers/Install.js
@@ -1,4 +1,5 @@
import React, {Component} from 'react';
+import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose} from 'react-apollo';
@@ -31,7 +32,7 @@ class InstallContainer extends Component {
}
InstallContainer.contextTypes = {
- router: React.PropTypes.object
+ router: PropTypes.object
};
const mapStateToProps = (state) => ({
diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js
index 38cf3e5a5..9d9428bc4 100644
--- a/client/coral-admin/src/routes/Moderation/components/Comment.js
+++ b/client/coral-admin/src/routes/Moderation/components/Comment.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Link} from 'react-router';
import {Icon} from 'coral-ui';
diff --git a/client/coral-admin/src/routes/Moderation/components/CommentCount.js b/client/coral-admin/src/routes/Moderation/components/CommentCount.js
index 75af89584..53d611ed6 100644
--- a/client/coral-admin/src/routes/Moderation/components/CommentCount.js
+++ b/client/coral-admin/src/routes/Moderation/components/CommentCount.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './CommentCount.css';
import t from 'coral-framework/services/i18n';
diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationHeader.js b/client/coral-admin/src/routes/Moderation/components/ModerationHeader.js
index 9cc0bfed9..b9bd0caff 100644
--- a/client/coral-admin/src/routes/Moderation/components/ModerationHeader.js
+++ b/client/coral-admin/src/routes/Moderation/components/ModerationHeader.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Icon} from 'coral-ui';
import styles from './styles.css';
import t from 'coral-framework/services/i18n';
diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js b/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js
index a28e90c65..25b90cf64 100644
--- a/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js
+++ b/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import CommentCount from './CommentCount';
import styles from './styles.css';
import {SelectField, Option} from 'react-mdl-selectfield';
diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js
index e8b5b8191..3197ddd78 100644
--- a/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js
+++ b/client/coral-admin/src/routes/Moderation/components/ModerationQueue.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import Comment from '../containers/Comment';
import styles from './styles.css';
diff --git a/client/coral-admin/src/routes/Moderation/components/Story.js b/client/coral-admin/src/routes/Moderation/components/Story.js
index 6c3a4b02f..eaad6c2a6 100644
--- a/client/coral-admin/src/routes/Moderation/components/Story.js
+++ b/client/coral-admin/src/routes/Moderation/components/Story.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './StorySearch.css';
const formatDate = (date) => {
diff --git a/client/coral-admin/src/routes/Moderation/components/StorySearch.js b/client/coral-admin/src/routes/Moderation/components/StorySearch.js
index 748f80b0f..7ac18f907 100644
--- a/client/coral-admin/src/routes/Moderation/components/StorySearch.js
+++ b/client/coral-admin/src/routes/Moderation/components/StorySearch.js
@@ -8,7 +8,7 @@ const StorySearch = (props) => {
const {
root: {
- assets = []
+ assets,
},
data: {loading}
} = props;
@@ -62,7 +62,7 @@ const StorySearch = (props) => {
{
loading
?
- : assets.map((story, i) => {
+ : assets.nodes.map((story, i) => {
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
return {
})
}
- {assets.length === 0 && No results
}
+ {assets.nodes.length === 0 && No results
}
diff --git a/client/coral-admin/src/routes/Moderation/containers/StorySearch.js b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js
index 04472d219..04aab2d84 100644
--- a/client/coral-admin/src/routes/Moderation/containers/StorySearch.js
+++ b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js
@@ -89,12 +89,14 @@ class StorySearchContainer extends React.Component {
export const withAssetSearchQuery = withQuery(gql`
query SearchStories($value: String = "") {
assets(query: {value: $value, limit: 10}) {
- id
- title
- url
- created_at
- closedAt
- author
+ nodes {
+ id
+ title
+ url
+ created_at
+ closedAt
+ author
+ }
}
}
`, {
diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js
index 5b931e6fc..d57b8f779 100644
--- a/client/coral-admin/src/routes/Stories/components/Stories.js
+++ b/client/coral-admin/src/routes/Stories/components/Stories.js
@@ -50,17 +50,22 @@ export default class Stories extends Component {
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
}
- onStatusClick = (closeStream, id, statusMenuOpen) => () => {
+ onStatusClick = (closeStream, id, statusMenuOpen) => async () => {
if (statusMenuOpen) {
this.setState((prev) => {
prev.statusMenus[id] = false;
return prev;
});
- this.props.updateAssetState(id, closeStream ? Date.now() : null)
- .then(() => {
- const {search, sort, filter, page} = this.state;
- this.props.fetchAssets(page, limit, search, sort, filter);
- });
+
+ try {
+ await this.props.updateAssetState(id, closeStream ? Date.now() : null);
+ const {search, sort, filter, page} = this.state;
+ this.props.fetchAssets(page, limit, search, sort, filter);
+ } catch (err) {
+
+ // TODO: handle error.
+ console.error(err);
+ }
} else {
this.setState((prev) => {
prev.statusMenus[id] = true;
diff --git a/client/coral-configure/components/Markdown.js b/client/coral-configure/components/Markdown.js
index 482467987..d9d1bc4a7 100644
--- a/client/coral-configure/components/Markdown.js
+++ b/client/coral-configure/components/Markdown.js
@@ -1,4 +1,5 @@
-import React, {PureComponent, PropTypes} from 'react';
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
import marked from 'marked';
const renderer = new marked.Renderer();
diff --git a/client/coral-embed-stream/src/components/CountdownSeconds.js b/client/coral-embed-stream/src/components/CountdownSeconds.js
index d350c803b..789b674b5 100644
--- a/client/coral-embed-stream/src/components/CountdownSeconds.js
+++ b/client/coral-embed-stream/src/components/CountdownSeconds.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
/**
diff --git a/client/coral-embed-stream/src/components/EditableCommentContent.js b/client/coral-embed-stream/src/components/EditableCommentContent.js
index 084a061e1..c256616f0 100644
--- a/client/coral-embed-stream/src/components/EditableCommentContent.js
+++ b/client/coral-embed-stream/src/components/EditableCommentContent.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {notifyForNewCommentStatus} from 'talk-plugin-commentbox/CommentBox';
import {CommentForm} from 'talk-plugin-commentbox/CommentForm';
import styles from './Comment.css';
@@ -38,10 +39,10 @@ export class EditableCommentContent extends React.Component {
maxCharCount: PropTypes.number,
// edit a comment, passed {{ body }}
- editComment: React.PropTypes.func,
+ editComment: PropTypes.func,
// called when editing should be stopped
- stopEditing: React.PropTypes.func,
+ stopEditing: PropTypes.func,
}
constructor(props) {
diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js
index a74028627..86d36d47e 100644
--- a/client/coral-embed-stream/src/components/Embed.js
+++ b/client/coral-embed-stream/src/components/Embed.js
@@ -1,4 +1,5 @@
import React from 'react';
+import PropTypes from 'prop-types';
import Stream from '../containers/Stream';
import Slot from 'coral-framework/components/Slot';
import {can} from 'coral-framework/services/perms';
@@ -90,8 +91,8 @@ export default class Embed extends React.Component {
}
Embed.propTypes = {
- data: React.PropTypes.shape({
- loading: React.PropTypes.bool,
- error: React.PropTypes.object
+ data: PropTypes.shape({
+ loading: PropTypes.bool,
+ error: PropTypes.object
}).isRequired
};
diff --git a/client/coral-embed-stream/src/components/IgnoreUserWizard.js b/client/coral-embed-stream/src/components/IgnoreUserWizard.js
index b7612b9f8..68b92ae29 100644
--- a/client/coral-embed-stream/src/components/IgnoreUserWizard.js
+++ b/client/coral-embed-stream/src/components/IgnoreUserWizard.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './Comment.css';
import {Button} from 'coral-ui';
diff --git a/client/coral-embed-stream/src/components/InactiveCommentLabel.js b/client/coral-embed-stream/src/components/InactiveCommentLabel.js
index 824692558..6655c4881 100644
--- a/client/coral-embed-stream/src/components/InactiveCommentLabel.js
+++ b/client/coral-embed-stream/src/components/InactiveCommentLabel.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import styles from './InactiveCommentLabel.css';
import {Icon} from 'coral-ui';
diff --git a/client/coral-embed-stream/src/components/NewCount.js b/client/coral-embed-stream/src/components/NewCount.js
index d93148e0c..71f15fbeb 100644
--- a/client/coral-embed-stream/src/components/NewCount.js
+++ b/client/coral-embed-stream/src/components/NewCount.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import t from 'coral-framework/services/i18n';
diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js
index 89e7b13ae..43176353f 100644
--- a/client/coral-embed-stream/src/components/Stream.js
+++ b/client/coral-embed-stream/src/components/Stream.js
@@ -308,7 +308,7 @@ Stream.propTypes = {
postComment: PropTypes.func.isRequired,
// edit a comment, passed (id, asset_id, { body })
- editComment: React.PropTypes.func
+ editComment: PropTypes.func
};
export default Stream;
diff --git a/client/coral-embed-stream/src/components/StreamTabPanel.js b/client/coral-embed-stream/src/components/StreamTabPanel.js
index a511ac555..50f54c2a0 100644
--- a/client/coral-embed-stream/src/components/StreamTabPanel.js
+++ b/client/coral-embed-stream/src/components/StreamTabPanel.js
@@ -15,8 +15,8 @@ class StreamTabPanel extends React.Component {
{loading
?
:
- {tabPanes}
-
+ {tabPanes}
+
}
);
diff --git a/client/coral-embed-stream/src/components/SuspendedAccount.js b/client/coral-embed-stream/src/components/SuspendedAccount.js
index df2b6162c..903b1bcf4 100644
--- a/client/coral-embed-stream/src/components/SuspendedAccount.js
+++ b/client/coral-embed-stream/src/components/SuspendedAccount.js
@@ -1,4 +1,5 @@
-import React, {Component, PropTypes} from 'react';
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import styles from './SuspendAccount.css';
import {Button} from 'coral-ui';
diff --git a/client/coral-embed-stream/src/components/TopRightMenu.js b/client/coral-embed-stream/src/components/TopRightMenu.js
new file mode 100644
index 000000000..fd4587356
--- /dev/null
+++ b/client/coral-embed-stream/src/components/TopRightMenu.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {IgnoreUserWizard} from './IgnoreUserWizard';
+import Toggleable from './Toggleable';
+
+// TopRightMenu appears as a dropdown in the top right of the comment.
+// when you click the down cehvron, it expands and shows IgnoreUserWizard
+// when you click 'cancel' in the wizard, it closes the menu
+export class TopRightMenu extends React.Component {
+ static propTypes = {
+
+ // comment on which this menu appears
+ comment: PropTypes.shape({
+ user: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ username: PropTypes.string.isRequired
+ }).isRequired
+ }).isRequired,
+ ignoreUser: PropTypes.func,
+
+ // show notification to the user (e.g. for errors)
+ notify: PropTypes.func.isRequired,
+ }
+ constructor(props) {
+ super(props);
+ this.state = {
+ timesReset: 0
+ };
+ }
+ render() {
+ const {comment, ignoreUser, notify} = this.props;
+
+ // timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable)
+ const reset = () => this.setState({timesReset: this.state.timesReset + 1});
+ const ignoreUserAndCloseMenuAndNotifyOnError = async ({id}) => {
+
+ // close menu
+ reset();
+
+ // ignore user
+ try {
+ await ignoreUser({id});
+ } catch (error) {
+ notify('error', 'Failed to ignore user');
+ throw error;
+ }
+ };
+ return (
+
+
+
+
+
+ );
+ }
+}
+
diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js
index 09470de14..ad281ce0c 100644
--- a/client/coral-embed-stream/src/graphql/index.js
+++ b/client/coral-embed-stream/src/graphql/index.js
@@ -109,7 +109,7 @@ export default {
},
mutations: {
PostComment: ({
- variables: {comment: {asset_id, body, parent_id, tags = []}},
+ variables: {input: {asset_id, body, parent_id, tags = []}},
state: {auth},
}) => ({
optimisticResponse: {
diff --git a/client/coral-framework/components/Markdown.js b/client/coral-framework/components/Markdown.js
index 482467987..d9d1bc4a7 100644
--- a/client/coral-framework/components/Markdown.js
+++ b/client/coral-framework/components/Markdown.js
@@ -1,4 +1,5 @@
-import React, {PureComponent, PropTypes} from 'react';
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
import marked from 'marked';
const renderer = new marked.Renderer();
diff --git a/client/coral-framework/components/MarkdownEditor.js b/client/coral-framework/components/MarkdownEditor.js
index 0d2088e51..65f603b0c 100644
--- a/client/coral-framework/components/MarkdownEditor.js
+++ b/client/coral-framework/components/MarkdownEditor.js
@@ -1,4 +1,5 @@
-import React, {Component, PropTypes} from 'react';
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
import SimpleMDE from 'simplemde';
import cn from 'classnames';
import noop from 'lodash/noop';
diff --git a/client/coral-framework/graphql/fragments.js b/client/coral-framework/graphql/fragments.js
index 110f0796b..5b3fe5498 100644
--- a/client/coral-framework/graphql/fragments.js
+++ b/client/coral-framework/graphql/fragments.js
@@ -6,6 +6,7 @@ export default {
'SetCommentStatusResponse',
'SuspendUserResponse',
'RejectUsernameResponse',
+ 'CreateCommentResponse',
'SetUserStatusResponse',
'CreateFlagResponse',
'EditCommentResponse',
diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js
index 4254400bb..c09ce8751 100644
--- a/client/coral-framework/graphql/mutations.js
+++ b/client/coral-framework/graphql/mutations.js
@@ -192,17 +192,17 @@ export const withSetUserStatus = withMutation(
export const withPostComment = withMutation(
gql`
- mutation PostComment($comment: CreateCommentInput!) {
- createComment(comment: $comment) {
+ mutation PostComment($input: CreateCommentInput!) {
+ createComment(input: $input) {
...CreateCommentResponse
}
}
`, {
props: ({mutate}) => ({
- postComment: (comment) => {
+ postComment: (input) => {
return mutate({
variables: {
- comment
+ input
},
});
}
diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js
index ac94d8b29..a3ae5b18e 100644
--- a/client/coral-framework/services/bootstrap.js
+++ b/client/coral-framework/services/bootstrap.js
@@ -15,6 +15,25 @@ import globalFragments from 'coral-framework/graphql/fragments';
import {createStorage} from 'coral-framework/services/storage';
import {createHistory} from 'coral-framework/services/history';
+/**
+ * getStaticConfiguration will return a singleton of the static configuration
+ * object provided via a JSON DOM element.
+ */
+const getStaticConfiguration = (() => {
+ let staticConfiguration = null;
+ return () => {
+ if (staticConfiguration != null) {
+ return staticConfiguration;
+ }
+
+ const configElement = document.querySelector('#data');
+
+ staticConfiguration = JSON.parse(configElement ? configElement.textContent : '{}');
+
+ return staticConfiguration;
+ };
+})();
+
/**
* getAuthToken returns the active auth token or null
* Note: this method does not have access to the cookie based token used by
@@ -49,7 +68,6 @@ const getAuthToken = (store, storage) => {
* @return {Object} context
*/
export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification} = {}) {
- const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
const eventEmitter = new EventEmitter({wildcard: true});
const storage = createStorage();
const history = createHistory(BASE_PATH);
@@ -63,13 +81,28 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi
// TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT.
return getAuthToken(store, storage);
};
+
const rest = createRestClient({
uri: `${BASE_PATH}api/v1`,
token,
});
+
+ // Try to get an overrided liveUri from the static config, if none is found,
+ // build it.
+ let {LIVE_URI: liveUri} = getStaticConfiguration();
+ if (liveUri == null) {
+
+ // The protocol must match the origin protocol, secure/insecure.
+ const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
+
+ // Compose the live url from this protocol, the current host + base path
+ // with the live path appended to it.
+ liveUri = `${protocol}://${location.host}${BASE_PATH}api/v1/live`;
+ }
+
const client = createClient({
uri: `${BASE_PATH}api/v1/graph/ql`,
- liveUri: `${protocol}://${location.host}${BASE_PATH}api/v1/live`,
+ liveUri,
token,
});
const plugins = createPluginsService(pluginsConfig);
@@ -79,6 +112,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi
// Use default notification service (pym based)
notification = createNotificationService(pym);
}
+
const context = {
client,
pym,
diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js
index 4424e62c5..037c1b99f 100644
--- a/client/coral-framework/utils/index.js
+++ b/client/coral-framework/utils/index.js
@@ -109,7 +109,7 @@ export function mergeDocuments(documents) {
export function getResponseErrors(mutationResult) {
const result = [];
Object.keys(mutationResult.data).forEach((response) => {
- const errors = mutationResult.data[response].errors;
+ const errors = mutationResult.data[response] && mutationResult.data[response].errors;
if (errors && errors.length) {
result.push(...errors);
}
diff --git a/client/coral-settings/components/IgnoredUsers.js b/client/coral-settings/components/IgnoredUsers.js
new file mode 100644
index 000000000..9cb314373
--- /dev/null
+++ b/client/coral-settings/components/IgnoredUsers.js
@@ -0,0 +1,44 @@
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
+import t from 'coral-framework/services/i18n';
+import styles from './IgnoredUsers.css';
+
+export class IgnoredUsers extends Component {
+ static propTypes = {
+ users: PropTypes.arrayOf(PropTypes.shape({
+ username: PropTypes.string,
+ id: PropTypes.string,
+ })).isRequired,
+
+ // accepts { id }
+ stopIgnoring: PropTypes.func.isRequired,
+ }
+ render() {
+ const {users, stopIgnoring} = this.props;
+ return (
+
+ );
+ }
+}
+
+export default IgnoredUsers;
diff --git a/client/coral-ui/components/CoralLogo.js b/client/coral-ui/components/CoralLogo.js
index 91331523f..ba640020e 100644
--- a/client/coral-ui/components/CoralLogo.js
+++ b/client/coral-ui/components/CoralLogo.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './CoralLogo.css';
const CoralLogo = ({className = ''}) => (
diff --git a/client/coral-ui/components/Dialog.js b/client/coral-ui/components/Dialog.js
index 6e2489fea..93431db4c 100644
--- a/client/coral-ui/components/Dialog.js
+++ b/client/coral-ui/components/Dialog.js
@@ -1,4 +1,5 @@
-import React, {Component, PropTypes} from 'react';
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
import dialogPolyfill from 'dialog-polyfill';
import 'dialog-polyfill/dialog-polyfill.css';
@@ -43,7 +44,7 @@ export default class Dialog extends Component {
componentWillUnmount() {
const dialog = this.dialog;
if (dialog) {
- dialog.removeEventListener('cancel', this.props.onCancel);
+ dialog.removeEventListener('cancel', this.props.onCancel);
}
}
diff --git a/client/coral-ui/components/Drawer.js b/client/coral-ui/components/Drawer.js
index 3c4f051bd..d50237bfb 100644
--- a/client/coral-ui/components/Drawer.js
+++ b/client/coral-ui/components/Drawer.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './Drawer.css';
const Drawer = ({children, onClose}) => {
diff --git a/client/coral-ui/components/Icon.js b/client/coral-ui/components/Icon.js
index 025b6df9e..e0dfea3a7 100644
--- a/client/coral-ui/components/Icon.js
+++ b/client/coral-ui/components/Icon.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Icon as IconMDL} from 'react-mdl';
import cn from 'classnames';
import styles from './Icon.css';
diff --git a/client/coral-ui/components/Pager.js b/client/coral-ui/components/Pager.js
index d14f2fc60..8f560593e 100644
--- a/client/coral-ui/components/Pager.js
+++ b/client/coral-ui/components/Pager.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './Pager.css';
const Rows = (curr, total, onClickHandler) => Array.from(Array(total)).map((e, i) =>
diff --git a/client/coral-ui/components/TextArea.js b/client/coral-ui/components/TextArea.js
index fedc7914b..547c0fc2e 100644
--- a/client/coral-ui/components/TextArea.js
+++ b/client/coral-ui/components/TextArea.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './TextArea.css';
const TextArea = ({className, value = '', ...props}) => (
diff --git a/client/coral-ui/components/TextField.js b/client/coral-ui/components/TextField.js
index ab5fc08ba..80c8173e7 100644
--- a/client/coral-ui/components/TextField.js
+++ b/client/coral-ui/components/TextField.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './TextField.css';
const TextField = ({className, showErrors = false, errorMsg, label, ...props}) => (
diff --git a/client/coral-ui/components/Wizard.js b/client/coral-ui/components/Wizard.js
index bc87a8bcc..97b056674 100644
--- a/client/coral-ui/components/Wizard.js
+++ b/client/coral-ui/components/Wizard.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
const Wizard = (props) => {
const {children, currentStep, ...rest} = props;
diff --git a/client/coral-ui/components/WizardNav.js b/client/coral-ui/components/WizardNav.js
index cbcb664be..c465c1da4 100644
--- a/client/coral-ui/components/WizardNav.js
+++ b/client/coral-ui/components/WizardNav.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './WizardNav.css';
import Icon from './Icon';
diff --git a/client/talk-plugin-commentbox/CommentBox.js b/client/talk-plugin-commentbox/CommentBox.js
index d4cc44f22..8bdfb9a24 100644
--- a/client/talk-plugin-commentbox/CommentBox.js
+++ b/client/talk-plugin-commentbox/CommentBox.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import {can} from 'coral-framework/services/perms';
@@ -54,7 +55,7 @@ class CommentBox extends React.Component {
return;
}
- let comment = {
+ let input = {
asset_id: assetId,
parent_id: parentId,
body: this.state.body,
@@ -62,10 +63,10 @@ class CommentBox extends React.Component {
};
// Execute preSubmit Hooks
- this.state.hooks.preSubmit.forEach((hook) => hook());
+ this.state.hooks.preSubmit.forEach((hook) => hook(input));
this.setState({loadingState: 'loading'});
- postComment(comment, 'comments')
+ postComment(input, 'comments')
.then(({data}) => {
this.setState({loadingState: 'success', body: ''});
const postedComment = data.createComment.comment;
diff --git a/client/talk-plugin-commentbox/CommentForm.js b/client/talk-plugin-commentbox/CommentForm.js
index fe0f9f6d9..f4bbbd139 100644
--- a/client/talk-plugin-commentbox/CommentForm.js
+++ b/client/talk-plugin-commentbox/CommentForm.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import cn from 'classnames';
import Slot from 'coral-framework/components/Slot';
diff --git a/client/talk-plugin-history/Comment.js b/client/talk-plugin-history/Comment.js
index b813e514b..51ab5c84a 100644
--- a/client/talk-plugin-history/Comment.js
+++ b/client/talk-plugin-history/Comment.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Icon} from '../coral-ui';
import styles from './Comment.css';
import Slot from 'coral-framework/components/Slot';
diff --git a/client/talk-plugin-history/CommentHistory.js b/client/talk-plugin-history/CommentHistory.js
index 91883cdc8..2e6f627e7 100644
--- a/client/talk-plugin-history/CommentHistory.js
+++ b/client/talk-plugin-history/CommentHistory.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import Comment from './Comment';
import styles from './CommentHistory.css';
import LoadMore from './LoadMore';
diff --git a/client/talk-plugin-moderation/ModerationLink.js b/client/talk-plugin-moderation/ModerationLink.js
index 2d9b8ff1e..6a2b43361 100644
--- a/client/talk-plugin-moderation/ModerationLink.js
+++ b/client/talk-plugin-moderation/ModerationLink.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import styles from './styles.css';
import t from 'coral-framework/services/i18n';
diff --git a/client/talk-plugin-replies/ReplyBox.js b/client/talk-plugin-replies/ReplyBox.js
index 50bd9a538..606aad000 100644
--- a/client/talk-plugin-replies/ReplyBox.js
+++ b/client/talk-plugin-replies/ReplyBox.js
@@ -1,4 +1,5 @@
-import React, {Component, PropTypes} from 'react';
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
import CommentBox from '../talk-plugin-commentbox/CommentBox';
const name = 'talk-plugin-replies';
diff --git a/client/talk-plugin-replies/ReplyButton.js b/client/talk-plugin-replies/ReplyButton.js
index 8ea113c97..13ff4de75 100644
--- a/client/talk-plugin-replies/ReplyButton.js
+++ b/client/talk-plugin-replies/ReplyButton.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
diff --git a/config.js b/config.js
index f48159444..d7e247ec3 100644
--- a/config.js
+++ b/config.js
@@ -135,6 +135,9 @@ const CONFIG = {
RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC,
RECAPTCHA_SECRET: process.env.TALK_RECAPTCHA_SECRET,
+ // WEBSOCKET_LIVE_URI is the absolute url to the live endpoint.
+ WEBSOCKET_LIVE_URI: process.env.TALK_WEBSOCKET_LIVE_URI || null,
+
//------------------------------------------------------------------------------
// SMTP Server configuration
//------------------------------------------------------------------------------
diff --git a/docs/_data/navigation.yml b/docs/_data/navigation.yml
index 7226f6003..9fe39d103 100755
--- a/docs/_data/navigation.yml
+++ b/docs/_data/navigation.yml
@@ -38,6 +38,8 @@ docs:
url: /docs/running/plugins/
- title: "Database Migrations"
url: /docs/running/migrations/
+ - title: "Persistence"
+ url: /docs/running/persistence/
- title: "Architecture"
url: /docs/architecture/
children:
diff --git a/docs/_docs/00-01-faq.md b/docs/_docs/00-01-faq.md
index 01172f84a..e114fce72 100644
--- a/docs/_docs/00-01-faq.md
+++ b/docs/_docs/00-01-faq.md
@@ -91,7 +91,7 @@ source process), then:
```
cd docs
-docker run --rm --volume=$PWD:/srv/jekyll -p 127.0.0.1:4000:4000 -it jekyll/jekyll:pages jekyll serve
+docker run --rm --volume=$PWD:/srv/jekyll -p 127.0.0.1:4000:4000 -it jekyll/jekyll:pages bash -c "bundle install && jekyll serve"
```
You can edit the files in docs with any editor and view the live updates in a
diff --git a/docs/_docs/02-01-configuration.md b/docs/_docs/02-01-configuration.md
index df790b24b..6ffb9afc0 100644
--- a/docs/_docs/02-01-configuration.md
+++ b/docs/_docs/02-01-configuration.md
@@ -53,6 +53,7 @@ These are only used during the webpack build.
- `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database.
#### Advanced
+{:.no_toc}
- `TALK_REDIS_RECONNECTION_MAX_ATTEMPTS` (_optional_) - the amount of attempts
that a redis connection will attempt to reconnect before aborting with an
@@ -76,11 +77,19 @@ These are only used during the webpack build.
- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - When `TRUE`, disables the dynamic setup endpoint. (Default `FALSE`)
#### Advanced
+{:.no_toc}
- `TALK_ROOT_URL_MOUNT_PATH` (_optional_) - when set to `TRUE`, the routes will
be mounted onto the `` component of the `TALK_ROOT_URL`. You would
use this when your upstream proxy cannot strip the prefix from the url.
(Default `FALSE`)
+- `TALK_WEBSOCKET_LIVE_URI` (_optional_) - used to override the location to
+ connect to the websocket endpoint to potentially another host. This should
+ be used when you need to route websocket requests out of your CDN in order to
+ serve traffic more efficiently for websockets. **Warning: if used without
+ managing the auth state manually, auth cannot be persisted, for further
+ information refer to the [Persistence Documentation]({{ "/docs/running/persistence/" | absolute_url }})**
+ (Default `${ssl ? 'ws' : 'wss'}://${location.host}${TALK_ROOT_URL_MOUNT_PATH}api/v1/live`)
### Word Filter
@@ -105,6 +114,7 @@ variable. Refer to the [Secrets Documentation]({{ "/docs/running/secrets/" | abs
on the contents of those variables.**
#### Advanced
+{:.no_toc}
These are advanced settings for fine tuning the auth integration, and
is not needed in most situations.
diff --git a/docs/_docs/02-05-persistence.md b/docs/_docs/02-05-persistence.md
new file mode 100644
index 000000000..a1d3a5e2f
--- /dev/null
+++ b/docs/_docs/02-05-persistence.md
@@ -0,0 +1,50 @@
+---
+title: User Persistence
+permalink: /docs/running/persistence/
+---
+
+One of the biggest problems on the internet today is the proliferation of
+sophisticated tracking systems and the outcome of invading user's privacy. This
+has had quite a big impact on our design of Talk, as we've had to work around
+the safeguards that are there to keep your data safe while still allowing our
+application to run smoothly on the page it's embedded on.
+
+---
+
+**Problem**: Safari has inconsistent behavior around localStorage when used
+within an iFrame.
+
+**Solution**: We set a cookie instead when Safari is detected to store the auth
+state.
+
+---
+
+**Problem**: Safari's default privacy settings block cookies from domains that
+do not match the current domain.
+
+**Solution**: When using Talk's built in auth, we will open a pop-out when
+setting the cookie, so that the domain of the setting domain matches the issuer.
+
+---
+
+**Problem**: When using a different domain for websockets, and using the built
+in auth solution, cookies are not set on that domain for use with Safari.
+
+**No Solution Exists**: It is our expectation that for users that must deploy
+Talk in environments that must run the websockets out of a separate domain will
+use and integrate their own auth solution. During the login process in Talk,
+users submit their user credentials to an auth endpoint, and receive a token
+back, or for Safari, a cookie. Aggressive defaults in Safari make it not
+possible to have one domain set cookies for another domain during this process.
+This results in a situation where we have no way to persist the auth credentials
+for this specific situation for the time being.
+
+---
+
+If you are using a custom auth solution, (Which involves providing the user's
+jwt token via the `auth_token` parameter on the call to
+`Talk.render(... {auth_token})` and creating the
+[tokenUserNotFound]({{ "/docs/plugins/server/" | absolute_url }}) hook to lookup
+that user) then concerns related to cookies/localStorage are moot! As it isn't
+necessary for Talk to maintain any state beyond what is passed to it via the
+pym bridge.
\ No newline at end of file
diff --git a/docs/_docs/04-04-plugins-server.md b/docs/_docs/04-04-plugins-server.md
index f807da141..e639cd308 100644
--- a/docs/_docs/04-04-plugins-server.md
+++ b/docs/_docs/04-04-plugins-server.md
@@ -85,7 +85,7 @@ configure the context plugin before it would be mounted at `context.plugins`.
This plugin above would mount at: `context.plugins.Slack`, or, if you're using
[object destructuring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), `{plugins: {Slack}}`.
-##### `Sort`
+##### Field: `Sort`
A special context hook, `Sort` will allow plugin authors to provide new
methods to sort data. An example is as follows:
@@ -269,22 +269,6 @@ The function is async, and should return the user object that was created in the
database, or null if the user wasn't found. The `jwt` parameter of the object
is the unpacked token, while `token` is the original jwt token string.
-### Routes
-
-#### Field: `router`
-
-```js
-(router) => {
- router.get('/api/v1/people', (req, res) => {
- res.json({people: [{name: 'Bob'}]});
- });
-}
-```
-
-The Router hook allows you to create a function that accepts the base express
-router where you can mount any amount of middleware/routes to do any form of
-action needed by external applications.
-
#### Field: `tags`
The tags hook allows a plugin to define tags that are code controlled (added
@@ -309,6 +293,22 @@ on how to create a hook for the `OFF_TOPIC` name:
You can refer to `models/schema/tag.js` for the available schema to match when
creating models to enable/disable specific features.
+### Routes
+
+#### Field: `router`
+
+```js
+(router) => {
+ router.get('/api/v1/people', (req, res) => {
+ res.json({people: [{name: 'Bob'}]});
+ });
+}
+```
+
+The Router hook allows you to create a function that accepts the base express
+router where you can mount any amount of middleware/routes to do any form of
+action needed by external applications.
+
### Authorization middleware
The following example creates the requisite callback route and passport
diff --git a/graph/errorHandler.js b/graph/errorHandler.js
new file mode 100644
index 000000000..d7176f16a
--- /dev/null
+++ b/graph/errorHandler.js
@@ -0,0 +1,54 @@
+const {forEachField} = require('./utils');
+const {maskErrors} = require('graphql-errors');
+const errors = require('../errors');
+const {Error: {ValidationError}} = require('mongoose');
+
+// If an APIError happens in a mutation, then respond with `{errors: Array}`
+// according to the schema.
+const decorateWithMutationErrorHandler = (field) => {
+ const fieldResolver = field.resolve;
+ field.resolve = async (obj, args, ctx, info) => {
+ try {
+ return await fieldResolver(obj, args, ctx, info);
+ }
+ 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;
+ }
+ };
+};
+
+/**
+ * Masks errors during production and handle mutation errors inside the schema.
+ * @param {GraphQLSchema} schema the schema to decorate
+ * @return {void}
+ */
+const decorateWithErrorHandler = (schema) => {
+ forEachField(schema, (field, typeName) => {
+
+ // Handle mutation errors.
+ if (typeName === 'RootMutation') {
+ decorateWithMutationErrorHandler(field);
+ }
+
+ // If we are in production mode, don't show server errors to the front end.
+ if (process.env.NODE_ENV === 'production') {
+
+ // Mask errors that are thrown if we are in a production environment.
+ maskErrors(field);
+ }
+ });
+};
+
+module.exports = {
+ decorateWithErrorHandler,
+};
diff --git a/graph/helpers/response.js b/graph/helpers/response.js
deleted file mode 100644
index 1db1e9b89..000000000
--- a/graph/helpers/response.js
+++ /dev/null
@@ -1,34 +0,0 @@
-const errors = require('../../errors');
-const {Error: {ValidationError}} = require('mongoose');
-
-/**
- * Wraps up a promise or value to return an object with the resolution of the promise
- * keyed at `key` or an error caught at `errors`.
- */
-
-const wrapResponse = (key) => async (promise) => {
- try {
- let value = await promise;
-
- 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 7f6be7feb..3a34be0bb 100644
--- a/graph/hooks.js
+++ b/graph/hooks.js
@@ -1,7 +1,4 @@
-const {
- GraphQLObjectType,
- GraphQLInterfaceType
-} = require('graphql');
+const {forEachField} = require('./utils');
const debug = require('debug')('talk:graph:schema');
const Joi = require('joi');
@@ -26,33 +23,6 @@ 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 field with the post resolvers (if available) and attaches a
* default type in the form of `Default${typeName}`.
@@ -239,6 +209,8 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
return result;
}, result);
};
+}, {
+ includeResolveType: true,
});
module.exports = {
diff --git a/graph/loaders/users.js b/graph/loaders/users.js
index ac3493479..b943c60ed 100644
--- a/graph/loaders/users.js
+++ b/graph/loaders/users.js
@@ -3,6 +3,10 @@ const DataLoader = require('dataloader');
const util = require('./util');
const union = require('lodash/union');
+const {
+ SEARCH_OTHER_USERS,
+} = require('../../perms/constants');
+
const UsersService = require('../../services/users');
const UserModel = require('../../models/user');
@@ -28,12 +32,23 @@ const genUserByIDs = async (context, ids) => {
* @param {Object} query query terms to apply to the users query
*/
const getUsersByQuery = async ({user, loaders: {Actions}}, {ids, limit, cursor, statuses, action_type, sortOrder}) => {
-
let query = UserModel.find();
- if (action_type) {
- const userIds = await Actions.getByTypes({action_type, item_type: 'USERS'});
- ids = ids ? union(ids, userIds) : userIds;
+ if (action_type || statuses) {
+ if (!user || !user.can(SEARCH_OTHER_USERS)) {
+ return null;
+ }
+
+ if (statuses) {
+ query = query.where({
+ status: {
+ $in: statuses
+ }
+ });
+ } else {
+ const userIds = await Actions.getByTypes({action_type, item_type: 'USERS'});
+ ids = ids ? union(ids, userIds) : userIds;
+ }
}
if (ids) {
@@ -44,14 +59,6 @@ const getUsersByQuery = async ({user, loaders: {Actions}}, {ids, limit, cursor,
});
}
- if (statuses) {
- query = query.where({
- status: {
- $in: statuses
- }
- });
- }
-
if (cursor) {
if (sortOrder === 'DESC') {
query = query.where({
diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js
index 4ca044fcf..9142687f6 100644
--- a/graph/mutators/comment.js
+++ b/graph/mutators/comment.js
@@ -154,7 +154,7 @@ const adjustKarma = (Comments, id, status) => async () => {
* @param {String} [status='NONE'] the status of the new comment
* @return {Promise} resolves to the created comment
*/
-const createComment = async (context, {tags = [], body, asset_id, parent_id = null}, status = 'NONE') => {
+const createComment = async (context, {tags = [], body, asset_id, parent_id = null, metadata = {}}, status = 'NONE') => {
const {user, loaders: {Comments}, pubsub} = context;
// Resolve the tags for the comment.
@@ -166,7 +166,8 @@ const createComment = async (context, {tags = [], body, asset_id, parent_id = nu
parent_id,
status,
tags,
- author_id: user.id
+ author_id: user.id,
+ metadata,
});
// If the loaders are present, clear the caches for these values because we
@@ -214,7 +215,7 @@ const filterNewComment = (context, {body, asset_id}) => {
* @param {Object} [wordlist={}] the results of the wordlist scan
* @return {Promise} resolves to the comment's status
*/
-const resolveNewCommentStatus = async (context, {asset_id, body}, wordlist = {}, settings = {}) => {
+const resolveNewCommentStatus = async (context, {asset_id, body, status}, wordlist = {}, settings = {}) => {
let {user} = context;
// Check to see if the body is too short, if it is, then complain about it!
@@ -269,7 +270,7 @@ const resolveNewCommentStatus = async (context, {asset_id, body}, wordlist = {},
}
}
- return moderation === 'PRE' ? 'PREMOD' : 'NONE';
+ return (moderation === 'PRE' || status === 'PREMOD') ? 'PREMOD' : 'NONE';
};
/**
diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js
index bfdc3991e..df67b8e8a 100644
--- a/graph/resolvers/root_mutation.js
+++ b/graph/resolvers/root_mutation.js
@@ -1,43 +1,41 @@
-const wrapResponse = require('../helpers/response');
-
const RootMutation = {
- createComment(_, {comment}, {mutators: {Comment}}) {
- return wrapResponse('comment')(Comment.create(comment));
+ createComment: async (_, {input}, {mutators: {Comment}}) => ({
+ comment: await Comment.create(input),
+ }),
+ editComment: async (_, {id, asset_id, edit: {body}}, {mutators: {Comment}}) => ({
+ comment: await Comment.edit({id, asset_id, edit: {body}}),
+ }),
+ createFlag: async (_, {flag: {item_id, item_type, reason, message}}, {mutators: {Action}}) => ({
+ flag: Action.create({item_id, item_type, action_type: 'FLAG', group_id: reason, metadata: {message}}),
+ }),
+ createDontAgree: async (_, {dontagree: {item_id, item_type, reason, message}}, {mutators: {Action}}) => ({
+ dontagree: await Action.create({item_id, item_type, action_type: 'DONTAGREE', group_id: reason, metadata: {message}}),
+ }),
+ deleteAction: async (_, {id}, {mutators: {Action}}) => {
+ await Action.delete({id});
},
- editComment(_, {id, asset_id, edit: {body}}, {mutators: {Comment}}) {
- return wrapResponse('comment')(Comment.edit({id, asset_id, edit: {body}}));
+ setUserStatus: async (_, {id, status}, {mutators: {User}}) => {
+ await User.setUserStatus({id, status});
},
- createFlag(_, {flag: {item_id, item_type, reason, message}}, {mutators: {Action}}) {
- return wrapResponse('flag')(Action.create({item_id, item_type, action_type: 'FLAG', group_id: reason, metadata: {message}}));
+ suspendUser: async (_, {input: {id, message, until}}, {mutators: {User}}) => {
+ await User.suspendUser({id, message, until});
},
- createDontAgree(_, {dontagree: {item_id, item_type, reason, message}}, {mutators: {Action}}) {
- return wrapResponse('dontagree')(Action.create({item_id, item_type, action_type: 'DONTAGREE', group_id: reason, metadata: {message}}));
+ rejectUsername: async (_, {input: {id, message}}, {mutators: {User}}) => {
+ await User.rejectUsername({id, message});
},
- deleteAction(_, {id}, {mutators: {Action}}) {
- return wrapResponse(null)(Action.delete({id}));
+ ignoreUser: async (_, {id}, {mutators: {User}}) => {
+ await User.ignoreUser({id});
},
- setUserStatus(_, {id, status}, {mutators: {User}}) {
- return wrapResponse(null)(User.setUserStatus({id, status}));
+ stopIgnoringUser: async (_, {id}, {mutators: {User}}) => {
+ await User.stopIgnoringUser({id});
},
- suspendUser(_, {input: {id, message, until}}, {mutators: {User}}) {
- return wrapResponse(null)(User.suspendUser({id, message, until}));
+ updateAssetSettings: async (_, {id, input: settings}, {mutators: {Asset}}) => {
+ await Asset.updateSettings(id, settings);
},
- rejectUsername(_, {input: {id, message}}, {mutators: {User}}) {
- return wrapResponse(null)(User.rejectUsername({id, message}));
+ updateAssetStatus: async (_, {id, input: status}, {mutators: {Asset}}) => {
+ await Asset.updateStatus(id, status);
},
- updateAssetSettings(_, {id, input: settings}, {mutators: {Asset}}) {
- return wrapResponse(null)(Asset.updateSettings(id, settings));
- },
- updateAssetStatus(_, {id, input: status}, {mutators: {Asset}}) {
- return wrapResponse(null)(Asset.updateStatus(id, status));
- },
- ignoreUser(_, {id}, {mutators: {User}}) {
- return wrapResponse(null)(User.ignoreUser({id}));
- },
- stopIgnoringUser(_, {id}, {mutators: {User}}) {
- return wrapResponse(null)(User.stopIgnoringUser({id}));
- },
- async setCommentStatus(_, {id, status}, {mutators: {Comment}, pubsub}) {
+ setCommentStatus: async (_, {id, status}, {mutators: {Comment}, pubsub}) => {
const comment = await Comment.setStatus({id, status});
if (status === 'ACCEPTED') {
@@ -48,25 +46,24 @@ const RootMutation = {
// Publish the comment status change via the subscription.
pubsub.publish('commentRejected', comment);
}
- return wrapResponse(null)(comment);
},
- addTag(_, {tag}, {mutators: {Tag}}) {
- return wrapResponse(null)(Tag.add(tag));
+ addTag: async (_, {tag}, {mutators: {Tag}}) => {
+ await Tag.add(tag);
},
- removeTag(_, {tag}, {mutators: {Tag}}) {
- return wrapResponse(null)(Tag.remove(tag));
+ removeTag: async (_, {tag}, {mutators: {Tag}}) => {
+ await Tag.remove(tag);
},
- updateSettings(_, {input: settings}, {mutators: {Settings}}) {
- return wrapResponse(null)(Settings.update(settings));
+ updateSettings: async (_, {input: settings}, {mutators: {Settings}}) => {
+ await Settings.update(settings);
},
- updateWordlist(_, {input: wordlist}, {mutators: {Settings}}) {
- return wrapResponse(null)(Settings.updateWordlist(wordlist));
+ updateWordlist: async (_, {input: wordlist}, {mutators: {Settings}}) => {
+ await Settings.updateWordlist(wordlist);
},
- createToken(_, {input}, {mutators: {Token}}) {
- return wrapResponse('token')(Token.create(input));
- },
- revokeToken(_, {input}, {mutators: {Token}}) {
- return wrapResponse(null)(Token.revoke(input));
+ createToken: async (_, {input}, {mutators: {Token}}) => ({
+ token: await Token.create(input),
+ }),
+ revokeToken: async (_, {input}, {mutators: {Token}}) => {
+ await Token.revoke(input);
}
};
diff --git a/graph/schema.js b/graph/schema.js
index 53360e701..d4cd9f890 100644
--- a/graph/schema.js
+++ b/graph/schema.js
@@ -1,6 +1,6 @@
const {makeExecutableSchema} = require('graphql-tools');
-const {maskErrors} = require('graphql-errors');
const {decorateWithHooks} = require('./hooks');
+const {decorateWithErrorHandler} = require('./errorHandler');
const plugins = require('../services/plugins');
const resolvers = require('./resolvers');
@@ -11,11 +11,7 @@ const schema = makeExecutableSchema({typeDefs, resolvers});
// Plugin to the schema level resolvers to provide an before/after hook.
decorateWithHooks(schema, plugins.get('server', 'hooks'));
-// If we are in production mode, don't show server errors to the front end.
-if (process.env.NODE_ENV === 'production') {
-
- // Mask errors that are thrown if we are in a production environment.
- maskErrors(schema);
-}
+// Handle errors like masking in production and mutation errors.
+decorateWithErrorHandler(schema);
module.exports = schema;
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index 24d814311..916c0ade5 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -1254,7 +1254,7 @@ type RevokeTokenResponse implements Response {
type RootMutation {
# Creates a comment on the asset.
- createComment(comment: CreateCommentInput!): CreateCommentResponse!
+ createComment(input: CreateCommentInput!): CreateCommentResponse!
# Creates a flag on an entity.
createFlag(flag: CreateFlagInput!): CreateFlagResponse!
@@ -1270,42 +1270,42 @@ type RootMutation {
# Sets User status. Requires the `ADMIN` role.
# Mutation is restricted.
- setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse!
+ setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse
# Suspends a user. Requires the `ADMIN` role.
# Mutation is restricted.
- suspendUser(input: SuspendUserInput!): SuspendUserResponse!
+ suspendUser(input: SuspendUserInput!): SuspendUserResponse
# Reject a username. Requires the `ADMIN` role.
# Mutation is restricted.
- rejectUsername(input: RejectUsernameInput!): RejectUsernameResponse!
+ rejectUsername(input: RejectUsernameInput!): RejectUsernameResponse
# Sets Comment status. Requires the `ADMIN` role.
# Mutation is restricted.
- setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse!
+ setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse
# Add a tag.
- addTag(tag: ModifyTagInput!): ModifyTagResponse!
+ addTag(tag: ModifyTagInput!): ModifyTagResponse
# Removes a tag.
- removeTag(tag: ModifyTagInput!): ModifyTagResponse!
+ removeTag(tag: ModifyTagInput!): ModifyTagResponse
# Updates settings on a given asset.
# Mutation is restricted.
- updateAssetSettings(id: ID!, input: AssetSettingsInput!): UpdateAssetSettingsResponse!
+ updateAssetSettings(id: ID!, input: AssetSettingsInput!): UpdateAssetSettingsResponse
# Updates the status of an asset allowing you to close/reopen an asset for
# commenting.
# Mutation is restricted.
- updateAssetStatus(id: ID!, input: UpdateAssetStatusInput!): UpdateAssetStatusResponse!
+ updateAssetStatus(id: ID!, input: UpdateAssetStatusInput!): UpdateAssetStatusResponse
# updateSettings will update the global settings.
# Mutation is restricted.
- updateSettings(input: UpdateSettingsInput!): UpdateSettingsResponse!
+ updateSettings(input: UpdateSettingsInput!): UpdateSettingsResponse
# updateWordlist will update the given Wordlist.
# Mutation is restricted.
- updateWordlist(input: UpdateWordlistInput!): UpdateWordlistResponse!
+ updateWordlist(input: UpdateWordlistInput!): UpdateWordlistResponse
# Ignore comments by another user
ignoreUser(id: ID!): IgnoreUserResponse
@@ -1316,10 +1316,10 @@ type RootMutation {
# RevokeToken will revoke an existing token.
# Mutation is restricted.
- revokeToken(input: RevokeTokenInput!): RevokeTokenResponse!
+ revokeToken(input: RevokeTokenInput!): RevokeTokenResponse
# Stop Ignoring comments by another user.
- stopIgnoringUser(id: ID!): StopIgnoringUserResponse!
+ stopIgnoringUser(id: ID!): StopIgnoringUserResponse
}
################################################################################
diff --git a/graph/utils.js b/graph/utils.js
new file mode 100644
index 000000000..d170a2d83
--- /dev/null
+++ b/graph/utils.js
@@ -0,0 +1,46 @@
+const {
+ GraphQLObjectType,
+ GraphQLInterfaceType
+} = require('graphql');
+
+/**
+ * Iterates over each field in a schema.
+ * 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. (Only when `options.includeResolveType` is set to true).
+ *
+ * @param {GraphQLSchema} schema the schema to iterate over
+ * @param {function} fn callback to call on each field
+ * @param {object} [options] options
+ * @param {boolean} [options.includeResolveType] include resolveType during iteration
+ * @return {void}
+ */
+const forEachField = (schema, fn, options = {}) => {
+ const {includeResolveType = false} = options;
+
+ 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 (includeResolveType && '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);
+ });
+ }
+ });
+};
+
+module.exports = {
+ forEachField,
+};
diff --git a/package.json b/package.json
index 8c45239e3..29ba80ae8 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,9 @@
{
"name": "talk",
- "version": "3.4.0",
+ "version": "3.5.0",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
+ "private": true,
"scripts": {
"postinstall": "./bin/cli plugins reconcile --skip-remote",
"start": "./bin/cli serve -j -w",
@@ -11,8 +12,8 @@
"build": "WEBPACK=TRUE NODE_ENV=production webpack -p --config webpack.config.js --bail",
"prebuild-watch": "yarn generate-introspection",
"build-watch": "WEBPACK=TRUE NODE_ENV=development webpack --progress --config webpack.config.js --watch",
- "lint": "eslint --ext .json bin/* .",
- "lint-fix": "eslint bin/* . --fix",
+ "lint": "eslint --ext=.js --ext=.json bin/* .",
+ "lint-fix": "yarn lint --fix",
"test": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
"test-cover": "TEST_MODE=unit NODE_ENV=test istanbul cover _mocha --report text --check-coverage -- -R spec",
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build",
diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js
index 02878e010..481990c16 100644
--- a/plugin-api/beta/server/getReactionConfig.js
+++ b/plugin-api/beta/server/getReactionConfig.js
@@ -1,4 +1,3 @@
-const wrapResponse = require('../../../graph/helpers/response');
const {SEARCH_OTHER_USERS} = require('../../../perms/constants');
const errors = require('../../../errors');
const pluralize = require('pluralize');
@@ -90,10 +89,6 @@ function getReactionConfig(reaction) {
}
type Delete${Reaction}ActionResponse implements Response {
-
- # The ${reaction} that was created.
- ${reaction}: ${Reaction}Action
-
# An array of errors relating to the mutation that occurred.
errors: [UserError!]
}
@@ -101,7 +96,7 @@ function getReactionConfig(reaction) {
type RootMutation {
# Creates a ${reaction} on an entity.
- create${Reaction}Action(input: Create${Reaction}ActionInput!): Create${Reaction}ActionResponse
+ create${Reaction}Action(input: Create${Reaction}ActionInput!): Create${Reaction}ActionResponse!
delete${Reaction}Action(input: Delete${Reaction}ActionInput!): Delete${Reaction}ActionResponse
}
@@ -160,7 +155,7 @@ function getReactionConfig(reaction) {
}
},
RootMutation: {
- [`create${Reaction}Action`]: (_, {input: {item_id}}, {mutators: {Action}, pubsub, loaders: {Comments}}) => wrapResponse(reaction)(async () => {
+ [`create${Reaction}Action`]: async (_, {input: {item_id}}, {mutators: {Action}, pubsub, loaders: {Comments}}) => {
const comment = await Comments.get.load(item_id);
let action;
@@ -180,9 +175,11 @@ function getReactionConfig(reaction) {
pubsub.publish(`${reaction}ActionCreated`, {action, comment});
}
- return action;
- }),
- [`delete${Reaction}Action`]: (_, {input: {id}}, {mutators: {Action}, pubsub, loaders: {Comments}}) => wrapResponse(reaction)(async () => {
+ return {
+ [reaction]: action,
+ };
+ },
+ [`delete${Reaction}Action`]: async (_, {input: {id}}, {mutators: {Action}, pubsub, loaders: {Comments}}) => {
const action = await Action.delete({id});
if (!action) {
return null;
@@ -194,9 +191,7 @@ function getReactionConfig(reaction) {
// The comment is needed to allow better filtering e.g. by asset_id.
pubsub.publish(`${reaction}ActionDeleted`, {action, comment});
}
-
- return action;
- })
+ },
},
},
hooks: {
diff --git a/plugins/talk-plugin-auth/client/components/SignInContent.js b/plugins/talk-plugin-auth/client/components/SignInContent.js
index 39c671ac2..d1b4caec1 100644
--- a/plugins/talk-plugin-auth/client/components/SignInContent.js
+++ b/plugins/talk-plugin-auth/client/components/SignInContent.js
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {Button, TextField, Spinner, Success, Alert} from 'plugin-api/beta/client/components/ui';
import styles from './styles.css';
import t from 'coral-framework/services/i18n';
diff --git a/plugins/talk-plugin-toxic-comments/client/.babelrc b/plugins/talk-plugin-toxic-comments/client/.babelrc
new file mode 100644
index 000000000..60be246eb
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/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/talk-plugin-toxic-comments/client/.eslintrc.json b/plugins/talk-plugin-toxic-comments/client/.eslintrc.json
new file mode 100644
index 000000000..9fe56bd14
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/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/talk-plugin-toxic-comments/client/components/CheckToxicityHook.js b/plugins/talk-plugin-toxic-comments/client/components/CheckToxicityHook.js
new file mode 100644
index 000000000..22c378435
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/client/components/CheckToxicityHook.js
@@ -0,0 +1,38 @@
+import React from 'react';
+
+/**
+ * CheckToxicityHook adds hooks to the `commentBox`
+ * that handles checking a comment for toxicity.
+ */
+export default class CheckToxicityHook extends React.Component {
+
+ // checked signifies if we already sent a request with the `checkToxicity` set to true.
+ checked = false;
+
+ componentDidMount() {
+ this.toxicityPreHook = this.props.registerHook('preSubmit', (input) => {
+
+ // If we haven't check the toxicity yet, make sure to include `checkToxicity=true` in the mutation.
+ // Otherwise post comment without checking the toxicity.
+ if (!this.checked) {
+ input.checkToxicity = true;
+ this.checked = true;
+ }
+ });
+
+ this.toxicityPostHook = this.props.registerHook('postSubmit', () => {
+
+ // Reset `checked` after comment was successfully posted.
+ this.checked = false;
+ });
+ }
+
+ componentWillUnmount() {
+ this.props.unregisterHook(this.toxicityPreHook);
+ this.props.unregisterHook(this.toxicityPostHook);
+ }
+
+ render() {
+ return null;
+ }
+}
diff --git a/plugins/talk-plugin-toxic-comments/client/index.js b/plugins/talk-plugin-toxic-comments/client/index.js
new file mode 100644
index 000000000..c364a2a00
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/client/index.js
@@ -0,0 +1,9 @@
+import translations from './translations.yml';
+import CheckToxicityHook from './components/CheckToxicityHook';
+
+export default {
+ translations,
+ slots: {
+ commentInputDetailArea: [CheckToxicityHook],
+ },
+};
diff --git a/plugins/talk-plugin-toxic-comments/client/translations.yml b/plugins/talk-plugin-toxic-comments/client/translations.yml
new file mode 100644
index 000000000..ebcc31903
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/client/translations.yml
@@ -0,0 +1,6 @@
+en:
+ error:
+ COMMENT_IS_TOXIC: |
+ Are you sure? The language in this comment might violate our community guidelines.
+ You can edit the comment or submit it for moderator review.
+es:
diff --git a/plugins/talk-plugin-toxic-comments/index.js b/plugins/talk-plugin-toxic-comments/index.js
new file mode 100644
index 000000000..0802a6c11
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/index.js
@@ -0,0 +1,8 @@
+const {readFileSync} = require('fs');
+const path = require('path');
+const hooks = require('./server/hooks');
+
+module.exports = {
+ typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'),
+ hooks,
+};
diff --git a/plugins/talk-plugin-toxic-comments/package.json b/plugins/talk-plugin-toxic-comments/package.json
new file mode 100644
index 000000000..848e6d948
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@coralproject/talk-plugin-toxicity",
+ "pluginName": "talk-plugin-toxicity",
+ "version": "0.0.1",
+ "description": "Provides support for measuring the toxicity of user comments using the Perspectives API",
+ "main": "index.js",
+ "author": "The Coral Project Team ",
+ "license": "Apache-2.0"
+}
diff --git a/plugins/talk-plugin-toxic-comments/server/config.js b/plugins/talk-plugin-toxic-comments/server/config.js
new file mode 100644
index 000000000..bc7ddf138
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/server/config.js
@@ -0,0 +1,12 @@
+const config = {
+ API_ENDPOINT: process.env.TALK_PERSPECTIVE_API_ENDPOINT || 'https://commentanalyzer.googleapis.com/v1alpha1',
+ API_KEY: process.env.TALK_PERSPECTIVE_API_KEY,
+ THRESHOLD: process.env.TALK_TOXICITY_THRESHOLD || 0.8,
+ API_TIMEOUT: process.env.TALK_PERSPECTIVE_TIMEOUT || 300,
+};
+
+if (process.env.NODE_ENV !== 'test' && !config.API_KEY) {
+ throw new Error('Please set the TALK_PERSPECTIVE_API_KEY environment variable to use the toxic-comments plugin. Visit https://www.perspectiveapi.com/ to request API access.');
+}
+
+module.exports = config;
diff --git a/plugins/talk-plugin-toxic-comments/server/errors.js b/plugins/talk-plugin-toxic-comments/server/errors.js
new file mode 100644
index 000000000..e5c77ff78
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/server/errors.js
@@ -0,0 +1,13 @@
+const {APIError} = require('errors');
+
+// ErrToxic is sent during a `CreateComment` mutation where
+// `input.checkToxicity` is set to true and the comment contains
+// toxic language as determined by the perspective service.
+const ErrToxic = new APIError('Comment is toxic', {
+ status: 400,
+ translation_key: 'COMMENT_IS_TOXIC',
+});
+
+module.exports = {
+ ErrToxic,
+};
diff --git a/plugins/talk-plugin-toxic-comments/server/hooks.js b/plugins/talk-plugin-toxic-comments/server/hooks.js
new file mode 100644
index 000000000..7fd138641
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/server/hooks.js
@@ -0,0 +1,67 @@
+const {getScores, isToxic} = require('./perspective');
+const {ErrToxic} = require('./errors');
+const ActionsService = require('../../../services/actions');
+
+// We don't add the hooks during _test_ as the perspective API is not available.
+if (process.env.NODE_ENV === 'test') {
+ return null;
+}
+
+module.exports = {
+ RootMutation: {
+ createComment: {
+ async pre(_, {input}, _context, _info) {
+
+ let scores;
+
+ // Try getting scores.
+ try {
+ scores = await getScores(input.body);
+ }
+ catch(err) {
+
+ // Warn and let mutation pass.
+ console.trace(err);
+ return;
+ }
+
+ const commentIsToxic = isToxic(scores);
+
+ if (input.checkToxicity && commentIsToxic) {
+ throw ErrToxic;
+ }
+
+ // attach scores to metadata.
+ input.metadata = Object.assign({}, input.metadata, {
+ perspective: scores,
+ });
+
+ if (commentIsToxic) {
+
+ // TODO: this should have a different status than Premod.
+ input.status = 'PREMOD';
+ }
+ },
+ async post(_, _input, _context, _info, result) {
+ const metadata = result.comment.metadata;
+ if (metadata.perspective && isToxic(metadata.perspective)) {
+
+ // TODO: this is kind of fragile, we should refactor this to resolve
+ // all these const's that we're using like 'COMMENTS', 'FLAG' to be
+ // defined in a checkable schema.
+
+ // Add a flag to the comment.
+ await ActionsService.create({
+ item_id: result.comment.id,
+ item_type: 'COMMENTS',
+ action_type: 'FLAG',
+ user_id: null,
+ group_id: 'Comment contains toxic language',
+ metadata: {}
+ });
+ }
+ return result;
+ },
+ },
+ },
+};
diff --git a/plugins/talk-plugin-toxic-comments/server/perspective.js b/plugins/talk-plugin-toxic-comments/server/perspective.js
new file mode 100644
index 000000000..fba81b3d2
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/server/perspective.js
@@ -0,0 +1,85 @@
+const fetch = require('node-fetch');
+const {API_ENDPOINT, API_KEY, THRESHOLD, API_TIMEOUT} = require('./config');
+
+/**
+ * Get scores from the perspective api
+ * @param {string} text text to be anaylized
+ * @return {object} object containing toxicity scores
+ */
+async function getScores(text) {
+ const response = await fetch(`${API_ENDPOINT}/comments:analyze?key=${API_KEY}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ timeout: API_TIMEOUT,
+ body: JSON.stringify({
+ comment: {
+ text,
+ },
+
+ // TODO: support other languages.
+ languages: ['en'],
+ requestedAttributes: {
+ TOXICITY: {},
+ SEVERE_TOXICITY: {},
+ }
+ }),
+ });
+ const data = await response.json();
+ return {
+ TOXICITY: {
+ summaryScore: data.attributeScores.TOXICITY.summaryScore.value
+ },
+ SEVERE_TOXICITY: {
+ summaryScore: data.attributeScores.SEVERE_TOXICITY.summaryScore.value
+ },
+ };
+}
+
+/**
+ * Get toxicity probability
+ * @param {object} scores scores as returned by `getScores`
+ * @return {number} toxicity probability from 0 - 1.0
+ */
+function getProbability(scores) {
+ return scores.SEVERE_TOXICITY.summaryScore;
+}
+
+/**
+ * isToxic determines if given probabilty or scores meets the toxicity threshold.
+ * @param {object|number} scoresOrProbability scores or probability
+ * @return {boolean}
+ */
+function isToxic(scoresOrProbability) {
+ const probability = typeof scoresOrProbability === 'object'
+ ? getProbability(scoresOrProbability)
+ : scoresOrProbability;
+ return probability > THRESHOLD;
+}
+
+/**
+ * maskKeyInError is a decorator that calls fn and masks the
+ * API_KEY in errors before throwing.
+ * @param {function} fn Function that returns a Promise
+ * @return {function} decorated function
+ */
+function maskKeyInError(fn) {
+ return async (...args) => {
+ try {
+ return await fn(...args);
+ }
+ catch(err) {
+ if (err.message) {
+ err.message = err.message.replace(API_KEY, '***');
+ }
+ throw err;
+ }
+ };
+}
+
+module.exports = {
+ getScores: maskKeyInError(getScores),
+ getProbability,
+ isToxic,
+};
diff --git a/plugins/talk-plugin-toxic-comments/server/typeDefs.graphql b/plugins/talk-plugin-toxic-comments/server/typeDefs.graphql
new file mode 100644
index 000000000..b4dfc8345
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/server/typeDefs.graphql
@@ -0,0 +1,7 @@
+input CreateCommentInput {
+
+ # If true, the mutation will fail when the
+ # body contains toxic language.
+ checkToxicity: Boolean
+}
+
diff --git a/plugins/talk-plugin-toxic-comments/yarn.lock b/plugins/talk-plugin-toxic-comments/yarn.lock
new file mode 100644
index 000000000..fb57ccd13
--- /dev/null
+++ b/plugins/talk-plugin-toxic-comments/yarn.lock
@@ -0,0 +1,4 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
diff --git a/routes/admin/index.js b/routes/admin/index.js
index 30bb69f5e..ab6189e85 100644
--- a/routes/admin/index.js
+++ b/routes/admin/index.js
@@ -1,7 +1,8 @@
const express = require('express');
const router = express.Router();
const {
- RECAPTCHA_PUBLIC
+ RECAPTCHA_PUBLIC,
+ WEBSOCKET_LIVE_URI,
} = require('../../config');
// Get /email-confirmation expects a signed JWT in the hash
@@ -20,7 +21,8 @@ router.get('/password-reset', (req, res) => {
router.get('*', (req, res) => {
const data = {
- TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC
+ TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC,
+ LIVE_URI: WEBSOCKET_LIVE_URI,
};
res.render('admin', {data});
diff --git a/routes/embed/index.js b/routes/embed/index.js
index 0aaa9ff9c..256d03b30 100644
--- a/routes/embed/index.js
+++ b/routes/embed/index.js
@@ -2,25 +2,24 @@ const express = require('express');
const router = express.Router();
const SettingsService = require('../../services/settings');
const {
- RECAPTCHA_PUBLIC
+ RECAPTCHA_PUBLIC,
+ WEBSOCKET_LIVE_URI,
} = require('../../config');
-router.use('/:embed', (req, res, next) => {
+router.use('/:embed', async (req, res, next) => {
switch (req.params.embed) {
- case 'stream':
- return SettingsService.retrieve()
- .then(({customCssUrl}) => {
- const data = {
- TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC
- };
+ case 'stream': {
+ const {customCssUrl} = await SettingsService.retrieve();
+ const data = {
+ TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC,
+ LIVE_URI: WEBSOCKET_LIVE_URI,
+ };
- return res.render('embed/stream', {customCssUrl, data});
- });
- default:
-
- // will return a 404.
- return next();
+ return res.render('embed/stream', {customCssUrl, data});
}
+ }
+
+ return next();
});
module.exports = router;
diff --git a/test/server/graph/mutations/createComment.js b/test/server/graph/mutations/createComment.js
index 8b99dd557..72c163d35 100644
--- a/test/server/graph/mutations/createComment.js
+++ b/test/server/graph/mutations/createComment.js
@@ -16,8 +16,8 @@ describe('graph.mutations.createComment', () => {
beforeEach(() => SettingsService.init());
const query = `
- mutation CreateComment($comment: CreateCommentInput = {asset_id: 123, body: "Here's my comment!"}) {
- createComment(comment: $comment) {
+ mutation CreateComment($input: CreateCommentInput = {asset_id: 123, body: "Here's my comment!"}) {
+ createComment(input: $input) {
comment {
id
status
@@ -176,7 +176,7 @@ describe('graph.mutations.createComment', () => {
const context = new Context({user: new UserModel({status: 'ACTIVE'})});
return graphql(schema, query, {}, context, {
- comment: {
+ input: {
asset_id: '123',
body
}
diff --git a/test/server/graph/mutations/removeTag.js b/test/server/graph/mutations/removeTag.js
index 4cb4f1e6a..761cd44c9 100644
--- a/test/server/graph/mutations/removeTag.js
+++ b/test/server/graph/mutations/removeTag.js
@@ -45,7 +45,7 @@ describe('graph.mutations.removeTag', () => {
console.error(response.errors);
}
expect(response.errors).to.be.empty;
- expect(response.data.removeTag.errors).to.be.null;
+ expect(response.data.removeTag).to.be.null;
let retrievedComment = await CommentsService.findById(comment.id);
diff --git a/test/server/graph/mutations/updateAssetSettings.js b/test/server/graph/mutations/updateAssetSettings.js
index 3fda0bd6c..306209c74 100644
--- a/test/server/graph/mutations/updateAssetSettings.js
+++ b/test/server/graph/mutations/updateAssetSettings.js
@@ -57,7 +57,10 @@ describe('graph.mutations.updateAssetSettings', () => {
expect(res.data.updateAssetSettings.errors).to.not.be.empty;
expect(res.data.updateAssetSettings.errors[0]).to.have.property('translation_key', error);
} else {
- expect(res.data.updateAssetSettings.errors).to.be.null;
+ if (res.data.updateAssetSettings && res.data.updateAssetSettings.errors) {
+ console.error(res.data.updateAssetSettings.errors);
+ }
+ expect(res.data.updateAssetSettings).to.be.null;
const retrievedAsset = await AssetModel.findOne({id: asset.id});
Object.keys(settings).forEach((key) => {
diff --git a/test/server/graph/mutations/updateAssetStatus.js b/test/server/graph/mutations/updateAssetStatus.js
index b1eb8874c..77be55976 100644
--- a/test/server/graph/mutations/updateAssetStatus.js
+++ b/test/server/graph/mutations/updateAssetStatus.js
@@ -55,7 +55,10 @@ describe('graph.mutations.updateAssetStatus', () => {
expect(res.data.updateAssetStatus.errors).to.not.be.empty;
expect(res.data.updateAssetStatus.errors[0]).to.have.property('translation_key', error);
} else {
- expect(res.data.updateAssetStatus.errors).to.be.null;
+ if (res.data.updateAssetStatus && res.data.updateAssetStatus.errors) {
+ console.error(res.data.updateAssetStatus.errors);
+ }
+ expect(res.data.updateAssetStatus).to.be.null;
const retrievedAsset = await AssetModel.findOne({id: asset.id});
expect(retrievedAsset.closedAt).to.not.be.null;
diff --git a/test/server/graph/mutations/updateSettings.js b/test/server/graph/mutations/updateSettings.js
index 27269b70a..c1ec678d6 100644
--- a/test/server/graph/mutations/updateSettings.js
+++ b/test/server/graph/mutations/updateSettings.js
@@ -58,10 +58,10 @@ describe('graph.mutations.updateSettings', () => {
expect(res.data.updateSettings.errors).to.not.be.empty;
expect(res.data.updateSettings.errors[0]).to.have.property('translation_key', error);
} else {
- if (res.data.updateSettings.errors) {
+ if (res.data.updateSettings && res.data.updateSettings.errors) {
console.error(res.data.updateSettings.errors);
}
- expect(res.data.updateSettings.errors).to.be.null;
+ expect(res.data.updateSettings).to.be.null;
const retrievedSettings = await SettingsService.retrieve();
Object.keys(newSettings).forEach((key) => {
diff --git a/test/server/graph/mutations/updateWordlist.js b/test/server/graph/mutations/updateWordlist.js
index b2f2e4abb..3a392a074 100644
--- a/test/server/graph/mutations/updateWordlist.js
+++ b/test/server/graph/mutations/updateWordlist.js
@@ -65,10 +65,10 @@ describe('graph.mutations.updateWordlist', () => {
expect(retrievedWordlist).to.have.property('suspect');
expect(retrievedWordlist.suspect).to.have.members([]);
} else {
- if (res.data.updateWordlist.errors) {
+ if (res.data.updateWordlist && res.data.updateWordlist.errors) {
console.error(res.data.updateWordlist.errors);
}
- expect(res.data.updateWordlist.errors).to.be.null;
+ expect(res.data.updateWordlist).to.be.null;
const {wordlist: retrievedWordlist} = await SettingsService.retrieve();
expect(retrievedWordlist).to.have.property('banned');
diff --git a/test/server/routes/api/assets/index.js b/test/server/routes/api/assets/index.js
index daccd5511..855b30271 100644
--- a/test/server/routes/api/assets/index.js
+++ b/test/server/routes/api/assets/index.js
@@ -3,7 +3,7 @@ const passport = require('../../../passport');
const app = require('../../../../../app');
const chai = require('chai');
-chai.should();
+chai.use(require('chai-as-promised'));
chai.use(require('chai-http'));
const expect = chai.expect;
@@ -13,139 +13,125 @@ const SettingsService = require('../../../../../services/settings');
describe('/api/v1/assets', () => {
- beforeEach(() => {
+ beforeEach(async () => {
const settings = {id: '1', moderation: 'PRE', domains: {whitelist: ['test.com']}};
- return SettingsService.init(settings).then(() => {
- return AssetModel.create([
- {
- url: 'https://coralproject.net/news/asset1',
- title: 'Asset 1',
- description: 'term1',
- closedAt: Date.now()
- },
- {
- url: 'https://coralproject.net/news/asset2',
- title: 'Asset 2',
- description: 'term2',
- closedAt: null
- }
- ]);
- });
+ await SettingsService.init(settings);
+
+ await AssetModel.create([
+ {
+ url: 'https://coralproject.net/news/asset1',
+ title: 'Asset 1',
+ description: 'term1',
+ closedAt: Date.now()
+ },
+ {
+ url: 'https://coralproject.net/news/asset2',
+ title: 'Asset 2',
+ description: 'term2',
+ closedAt: null
+ }
+ ]);
});
describe('#get', () => {
- it('should return all assets without a search query', () => {
- return chai.request(app)
+ it('should return all assets without a search query', async () => {
+ const res = await chai.request(app)
.get('/api/v1/assets')
- .set(passport.inject({roles: ['ADMIN']}))
- .then((res) => {
- const body = res.body;
+ .set(passport.inject({roles: ['ADMIN']}));
- expect(body).to.have.property('count', 2);
- expect(body).to.have.property('result');
+ const body = res.body;
- const assets = body.result;
+ expect(body).to.have.property('count', 2);
+ expect(body).to.have.property('result');
- expect(assets).to.have.length(2);
- });
+ const assets = body.result;
+
+ expect(assets).to.have.length(2);
});
- it('should return assets that we search for', () => {
- return chai.request(app)
+ it('should return assets that we search for', async () => {
+ const res = await chai.request(app)
.get('/api/v1/assets?search=term2')
- .set(passport.inject({roles: ['ADMIN']}))
- .then((res) => {
- const body = res.body;
+ .set(passport.inject({roles: ['ADMIN']}));
- expect(body).to.have.property('count', 1);
- expect(body).to.have.property('result');
+ const body = res.body;
- const assets = body.result;
+ expect(body).to.have.property('count', 1);
+ expect(body).to.have.property('result');
- expect(assets).to.have.length(1);
+ const assets = body.result;
- const asset = assets[0];
+ expect(assets).to.have.length(1);
- expect(asset).to.have.property('url', 'https://coralproject.net/news/asset2');
- expect(asset).to.have.property('title', 'Asset 2');
- });
+ const asset = assets[0];
+
+ expect(asset).to.have.property('url', 'https://coralproject.net/news/asset2');
+ expect(asset).to.have.property('title', 'Asset 2');
});
- it('should not return assets that we do not search for', () => {
- return chai.request(app)
+ it('should not return assets that we do not search for', async () => {
+ const res = await chai.request(app)
.get('/api/v1/assets?search=term3')
- .set(passport.inject({roles: ['ADMIN']}))
- .then((res) => {
- const body = res.body;
+ .set(passport.inject({roles: ['ADMIN']}));
+ const body = res.body;
- expect(body).to.have.property('count', 0);
- expect(body).to.have.property('result');
+ expect(body).to.have.property('count', 0);
+ expect(body).to.have.property('result');
- expect(body.result).to.be.empty;
- });
+ expect(body.result).to.be.empty;
});
- it('should return only closed assets', () => {
- return chai.request(app)
+ it('should return only closed assets', async () => {
+ const res = await chai.request(app)
.get('/api/v1/assets?filter=closed')
- .set(passport.inject({roles: ['ADMIN']}))
- .then((res) => {
- const body = res.body;
+ .set(passport.inject({roles: ['ADMIN']}));
+ const body = res.body;
- expect(body).to.have.property('count', 1);
- expect(body).to.have.property('result');
+ expect(body).to.have.property('count', 1);
+ expect(body).to.have.property('result');
- const assets = body.result;
+ const assets = body.result;
- expect(assets[0]).to.have.property('title', 'Asset 1');
- });
+ expect(assets[0]).to.have.property('title', 'Asset 1');
});
- it('should return only opened assets', () => {
- return chai.request(app)
+ it('should return only opened assets', async () => {
+ const res = await chai.request(app)
.get('/api/v1/assets?filter=open')
- .set(passport.inject({roles: ['ADMIN']}))
- .then((res) => {
- const body = res.body;
+ .set(passport.inject({roles: ['ADMIN']}));
+ const body = res.body;
- expect(body).to.have.property('count', 1);
- expect(body).to.have.property('result');
+ expect(body).to.have.property('count', 1);
+ expect(body).to.have.property('result');
- const assets = body.result;
+ const assets = body.result;
- expect(assets[0]).to.have.property('title', 'Asset 2');
- });
+ expect(assets[0]).to.have.property('title', 'Asset 2');
});
});
describe('#put', () => {
- it('should close the asset', function() {
+ it('should close the asset', async () => {
const today = Date.now();
- return AssetsService.findOrCreateByUrl('http://test.com')
- .then((asset) => {
- expect(asset).to.have.property('isClosed', false);
- expect(asset).to.have.property('closedAt', null);
+ const asset = await AssetsService.findOrCreateByUrl('http://test.com');
+ expect(asset).to.have.property('isClosed', false);
+ expect(asset).to.have.property('closedAt', null);
- return chai.request(app)
- .put(`/api/v1/assets/${asset.id}/status`)
- .set(passport.inject({roles: ['ADMIN']}))
- .send({closedAt: today});
- })
- .then((res) => {
+ const res = await chai.request(app)
+ .put(`/api/v1/assets/${asset.id}/status`)
+ .set(passport.inject({roles: ['ADMIN']}))
+ .send({closedAt: today});
- expect(res).to.have.status(204);
+ expect(res).to.have.status(204);
- return AssetsService.findByUrl('http://test.com');
- })
- .then((asset) => {
- expect(asset).to.have.property('isClosed', true);
- expect(asset).to.have.property('closedAt').and.to.not.equal(null);
- });
+ const closedAsset = await AssetsService.findByUrl('http://test.com');
+ expect(closedAsset).to.have.property('isClosed', true);
+ expect(closedAsset).to.have.property('closedAt').and.to.not.equal(null);
});
});