Merge branch 'master' of github.com:coralproject/talk into reject-instream

This commit is contained in:
Belen Curcio
2017-09-08 13:49:54 -03:00
115 changed files with 1041 additions and 439 deletions
+1
View File
@@ -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
View File
@@ -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",
+2
View File
@@ -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 -1
View File
@@ -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';
+13 -13
View File
@@ -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) => (
+2 -1
View File
@@ -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',
+4 -4
View File
@@ -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
View File
@@ -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,
+1 -1
View File
@@ -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;
+2 -1
View File
@@ -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 = ''}) => (
+3 -2
View File
@@ -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);
}
}
+2 -1
View File
@@ -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}) => {
+2 -1
View File
@@ -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';
+2 -1
View File
@@ -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) =>
+2 -1
View File
@@ -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}) => (
+2 -1
View File
@@ -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}) => (
+2 -1
View File
@@ -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;
+2 -1
View File
@@ -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';
+5 -4
View File
@@ -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;
+2 -1
View File
@@ -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';
+2 -1
View File
@@ -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';
+2 -1
View File
@@ -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';
+2 -1
View File
@@ -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';
+2 -1
View File
@@ -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';
+3
View File
@@ -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
//------------------------------------------------------------------------------
+2
View File
@@ -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:
+1 -1
View File
@@ -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
+10
View File
@@ -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.
+50
View File
@@ -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.
+17 -17
View File
@@ -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
+54
View File
@@ -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,
};
-34
View File
@@ -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
View File
@@ -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
View File
@@ -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({
+5 -4
View File
@@ -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';
};
/**
+42 -45
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
################################################################################
+46
View File
@@ -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
View File
@@ -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",
+8 -13
View File
@@ -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