mirror of
https://github.com/wassname/talk.git
synced 2026-07-12 01:57:58 +08:00
Merge branch 'master' of github.com:coralproject/talk into reject-instream
This commit is contained in:
@@ -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
|
||||
|
||||
+11
-23
@@ -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",
|
||||
|
||||
@@ -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/*
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
<Slot
|
||||
fill="userProfile"
|
||||
data={this.props.data}
|
||||
queryData={root, user}
|
||||
queryData={{root, user}}
|
||||
/>
|
||||
|
||||
<hr/>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = ({
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -85,7 +85,7 @@ class User extends React.Component {
|
||||
<span className={styles.flaggedByLabel}>
|
||||
{t('community.flags')}({ user.actions.length })
|
||||
</span>:
|
||||
{ user.action_summaries.map(
|
||||
{ user.action_summaries.map(
|
||||
(action, i) => {
|
||||
return <span className={styles.flaggedBy} key={i}>
|
||||
{shortReasons[action.reason]} ({action.count})
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ const StorySearch = (props) => {
|
||||
|
||||
const {
|
||||
root: {
|
||||
assets = []
|
||||
assets,
|
||||
},
|
||||
data: {loading}
|
||||
} = props;
|
||||
@@ -62,7 +62,7 @@ const StorySearch = (props) => {
|
||||
{
|
||||
loading
|
||||
? <Spinner />
|
||||
: assets.map((story, i) => {
|
||||
: assets.nodes.map((story, i) => {
|
||||
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
|
||||
|
||||
return <Story
|
||||
@@ -77,7 +77,7 @@ const StorySearch = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
{assets.length === 0 && <div className={styles.noResults}>No results</div>}
|
||||
{assets.nodes.length === 0 && <div className={styles.noResults}>No results</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,8 +15,8 @@ class StreamTabPanel extends React.Component {
|
||||
{loading
|
||||
? <div className={styles.spinnerContainer}><Spinner /></div>
|
||||
: <TabContent activeTab={activeTab} sub={sub}>
|
||||
{tabPanes}
|
||||
</TabContent>
|
||||
{tabPanes}
|
||||
</TabContent>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 (
|
||||
<Toggleable key={this.state.timesReset} className="talk-stream-comment-chevron">
|
||||
<div style={{position: 'absolute', right: 0, zIndex: 1}}>
|
||||
<IgnoreUserWizard
|
||||
user={comment.user}
|
||||
cancel={reset}
|
||||
ignoreUser={ignoreUserAndCloseMenuAndNotifyOnError}
|
||||
/>
|
||||
</div>
|
||||
</Toggleable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -6,6 +6,7 @@ export default {
|
||||
'SetCommentStatusResponse',
|
||||
'SuspendUserResponse',
|
||||
'RejectUsernameResponse',
|
||||
'CreateCommentResponse',
|
||||
'SetUserStatusResponse',
|
||||
'CreateFlagResponse',
|
||||
'EditCommentResponse',
|
||||
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+36
-2
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
{
|
||||
users.length
|
||||
? <p>{t('framework.because_you_ignored')}</p>
|
||||
: null
|
||||
}
|
||||
<dl className={styles.ignoredUserList}>
|
||||
{
|
||||
users.map(({username, id}) => (
|
||||
<span className={styles.ignoredUser} key={id}>
|
||||
<dt key={id}>{ username }</dt>
|
||||
<dd className={styles.stopListening}>
|
||||
<a
|
||||
onClick={() => stopIgnoring({id})}
|
||||
className={styles.link}>{t('framework.stop_ignoring')}</a>
|
||||
</dd>
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default IgnoredUsers;
|
||||
@@ -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 = ''}) => (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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}) => (
|
||||
|
||||
@@ -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}) => (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 `<pathname>` 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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
+3
-31
@@ -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 = {
|
||||
|
||||
+19
-12
@@ -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({
|
||||
|
||||
@@ -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';
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+3
-7
@@ -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;
|
||||
|
||||
+13
-13
@@ -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
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
+4
-3
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import translations from './translations.yml';
|
||||
import CheckToxicityHook from './components/CheckToxicityHook';
|
||||
|
||||
export default {
|
||||
translations,
|
||||
slots: {
|
||||
commentInputDetailArea: [CheckToxicityHook],
|
||||
},
|
||||
};
|
||||
@@ -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:
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 <coral@mozillafoundation.org>",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user