Merge branch 'master' into feature/talk-plugin-toxic-comments

This commit is contained in:
Kiwi
2017-09-05 12:56:27 +02:00
committed by GitHub
72 changed files with 1282 additions and 892 deletions
+3
View File
@@ -19,5 +19,8 @@ plugins/*
!plugins/talk-plugin-sort-most-liked
!plugins/talk-plugin-sort-most-loved
!plugins/talk-plugin-sort-most-respected
!plugins/talk-plugin-author-menu
!plugins/talk-plugin-member-since
!plugins/talk-plugin-ignore-user
node_modules
+3
View File
@@ -36,6 +36,9 @@ plugins/*
!plugins/talk-plugin-sort-most-liked
!plugins/talk-plugin-sort-most-loved
!plugins/talk-plugin-sort-most-respected
!plugins/talk-plugin-author-menu
!plugins/talk-plugin-member-since
!plugins/talk-plugin-ignore-user
**/node_modules/*
story.html
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import t from 'coral-framework/services/i18n';
import {join} from 'path';
import styles from './Configure.css';
import {Button, Card} from 'coral-ui';
import {BASE_URL} from 'coral-framework/constants/url';
@@ -25,7 +26,7 @@ class EmbedLink extends Component {
}
render () {
const coralJsUrl = [BASE_URL, '/embed.js'].join('');
const coralJsUrl = join(BASE_URL, '/embed.js');
const nonce = String(Math.random()).slice(2);
const streamElementId = `coral_talk_${nonce}`;
const embedText = `
@@ -208,17 +208,22 @@ class ModerationContainer extends Component {
// Still loading.
return <Spinner />;
}
} else if(asset !== undefined || !('premodCount' in root)) {
} else if (asset !== undefined || !('premodCount' in root)) {
// loading.
return <Spinner />;
}
const premodEnabled = assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation);
const premodEnabled = assetId ? isPremod(asset.settings.moderation) :
isPremod(settings.moderation);
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
if (premodEnabled) {
if (premodEnabled && root.newCount === 0) {
delete currentQueueConfig.new;
} else {
}
if (!premodEnabled && root.premodCount === 0) {
delete currentQueueConfig.premod;
}
@@ -355,8 +360,9 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
variables: {
asset_id: id,
sortOrder: props.moderation.sortOrder,
allAssets: id === null
}
allAssets: id === null,
},
fetchPolicy: 'network-only'
};
},
});
@@ -69,15 +69,15 @@ function getCommentQueues(comment, queueConfig) {
const queues = [];
Object.keys(queueConfig).forEach((key) => {
const {action_type, statuses, tags} = queueConfig[key];
let addToQueues = false;
if (statuses && statuses.indexOf(comment.status) >= 0) {
addToQueues = true;
let addToQueues = true;
if (statuses && statuses.indexOf(comment.status) === -1) {
addToQueues = false;
}
if (tags && comment.tags && comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0)) {
addToQueues = true;
if (tags && (!comment.tags || !comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0))) {
addToQueues = false;
}
if (action_type && comment.actions && comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type}action`)) {
addToQueues = true;
if (action_type && (!comment.actions || !comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type.toLowerCase()}action`))) {
addToQueues = false;
}
if (addToQueues) {
queues.push(key);
@@ -1,16 +1,16 @@
import t from 'coral-framework/services/i18n';
export default {
new: {
statuses: ['NONE'],
icon: 'question_answer',
name: t('modqueue.new'),
},
premod: {
statuses: ['PREMOD'],
icon: 'access_time',
name: t('modqueue.premod'),
},
new: {
statuses: ['NONE', 'PREMOD'],
icon: 'question_answer',
name: t('modqueue.new'),
},
reported: {
action_type: 'FLAG',
statuses: ['NONE', 'PREMOD'],
@@ -1,7 +1,6 @@
import React from 'react';
import LoadMore from './LoadMore';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import NewCount from './NewCount';
import {TransitionGroup} from 'react-transition-group';
import {forEachError} from 'coral-framework/utils';
@@ -126,7 +125,6 @@ class AllCommentsPane extends React.Component {
root,
comments,
commentClassNames,
ignoreUser,
setActiveReplyBox,
activeReplyBox,
notify,
@@ -139,7 +137,6 @@ class AllCommentsPane extends React.Component {
loadNewReplies,
deleteAction,
showSignInDialog,
commentIsIgnored,
charCountEnable,
maxCharCount,
editComment,
@@ -157,9 +154,8 @@ class AllCommentsPane extends React.Component {
/>
<TransitionGroup component='div' className="embed__stream">
{view.map((comment) => {
return commentIsIgnored(comment)
? <IgnoredCommentTombstone key={comment.id} />
: <Comment
return (
<Comment
commentClassNames={commentClassNames}
data={data}
root={root}
@@ -173,8 +169,6 @@ class AllCommentsPane extends React.Component {
currentUser={currentUser}
postFlag={postFlag}
postDontAgree={postDontAgree}
ignoreUser={ignoreUser}
commentIsIgnored={commentIsIgnored}
loadMore={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
@@ -184,7 +178,8 @@ class AllCommentsPane extends React.Component {
maxCharCount={maxCharCount}
editComment={editComment}
emit={emit}
/>;
/>
);
})}
</TransitionGroup>
<LoadMore
@@ -66,17 +66,6 @@
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
.topRight .popover {
margin-top: 1em;
right: 0px;
}
.topRight .link.active,
.topRight .active .link {
padding-bottom: 0.125em;
border-bottom: 2px solid currentColor;
}
.editCommentForm {
margin-bottom: 10px;
}
@@ -175,4 +164,4 @@
.username {
margin-right: 5px;
}
}
@@ -16,7 +16,6 @@ import mapValues from 'lodash/mapValues';
import LoadMore from './LoadMore';
import {getEditableUntilDate} from './util';
import {findCommentWithId} from '../graphql/utils';
import {TopRightMenu} from './TopRightMenu';
import CommentContent from './CommentContent';
import Slot from 'coral-framework/components/Slot';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
@@ -95,8 +94,7 @@ export default class Comment extends React.Component {
this.onClickEdit = this.onClickEdit.bind(this);
this.stopEditing = this.stopEditing.bind(this);
this.state = {
// Whether the comment should be editable (e.g. after a commenter clicking the 'Edit' button on their own comment)
isEditable: commentIsStillEditable(props.comment),
isEditing: false,
replyBoxVisible: false,
loadingState: '',
@@ -185,12 +183,6 @@ export default class Comment extends React.Component {
})
}).isRequired,
// given a comment, return whether it should be rendered as ignored
commentIsIgnored: PropTypes.func,
// dispatch action to ignore another user
ignoreUser: PropTypes.func,
// edit a comment, passed (id, asset_id, { body })
editComment: PropTypes.func,
@@ -217,9 +209,18 @@ export default class Comment extends React.Component {
}
}
commentIsIgnored(comment) {
const me = this.props.root.me;
return (
me &&
me.ignoredUsers &&
me.ignoredUsers.find((u) => u.id === comment.user.id)
);
}
hasIgnoredReplies() {
return this.props.comment.replies &&
this.props.comment.replies.nodes.some((reply) => this.props.commentIsIgnored(reply));
this.props.comment.replies.nodes.some((reply) => this.commentIsIgnored(reply));
}
loadNewReplies = () => {
@@ -295,14 +296,12 @@ export default class Comment extends React.Component {
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
}
// if still in the edit window, set a timeout to re-render once it expires
const msLeftToEdit = editWindowRemainingMs(this.props.comment);
if (msLeftToEdit > 0) {
// if still in the edit window, set a timeout to handle expiration.
if (this.state.isEditable) {
const msLeftToEdit = editWindowRemainingMs(this.props.comment);
this.editWindowExpiryTimeout = setTimeout(() => {
// re-render
this.setState(this.state);
}, msLeftToEdit);
this.setState({isEditable: false});
}, Math.max(msLeftToEdit, 0));
}
}
componentWillUnmount() {
@@ -320,7 +319,6 @@ export default class Comment extends React.Component {
comment,
postFlag,
parentId,
ignoreUser,
highlighted,
postComment,
currentUser,
@@ -335,12 +333,15 @@ export default class Comment extends React.Component {
charCountEnable,
showSignInDialog,
liveUpdates,
commentIsIgnored,
animateEnter,
emit,
commentClassNames = []
} = this.props;
if (this.commentIsIgnored(comment)) {
return <IgnoredCommentTombstone />;
}
const view = this.getVisibileReplies();
// Inactive comments can be viewed by moderators and admins (e.g. using permalinks).
@@ -476,23 +477,13 @@ export default class Comment extends React.Component {
/* User can edit/delete their own comment for a short window after posting */
<span className={cn(styles.topRight)}>
{
commentIsStillEditable(comment) &&
this.state.isEditable &&
<a
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
}
</span>
}
{ isActive && (currentUser && (comment.user.id !== currentUser.id)) &&
/* TopRightMenu allows currentUser to ignore other users' comments */
<span className={cn(styles.topRight, styles.topRightMenu)}>
<TopRightMenu
comment={comment}
ignoreUser={ignoreUser}
notify={notify} />
</span>
}
{ !isActive &&
<InactiveCommentLabel status={comment.status}/>
}
@@ -584,9 +575,8 @@ export default class Comment extends React.Component {
<TransitionGroup>
{view.map((reply) => {
return commentIsIgnored(reply)
? <IgnoredCommentTombstone key={reply.id} />
: <CommentContainer
return (
<CommentContainer
data={this.props.data}
root={this.props.root}
setActiveReplyBox={setActiveReplyBox}
@@ -603,17 +593,16 @@ export default class Comment extends React.Component {
postFlag={postFlag}
deleteAction={deleteAction}
loadMore={loadMore}
ignoreUser={ignoreUser}
charCountEnable={charCountEnable}
maxCharCount={maxCharCount}
showSignInDialog={showSignInDialog}
commentIsIgnored={commentIsIgnored}
liveUpdates={liveUpdates}
reactKey={reply.id}
key={reply.id}
comment={reply}
emit={emit}
/>;
/>
);
})}
</TransitionGroup>
<div className="talk-load-more-replies">
@@ -40,15 +40,6 @@ class Stream extends React.Component {
}
}
commentIsIgnored = (comment) => {
const me = this.props.root.me;
return (
me &&
me.ignoredUsers &&
me.ignoredUsers.find((u) => u.id === comment.user.id)
);
};
renderHighlightedComment() {
const {
data,
@@ -64,7 +55,6 @@ class Stream extends React.Component {
postDontAgree,
deleteAction,
showSignInDialog,
ignoreUser,
loadNewReplies,
auth: {user},
emit,
@@ -90,7 +80,6 @@ class Stream extends React.Component {
data={data}
root={root}
commentClassNames={commentClassNames}
ignoreUser={ignoreUser}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
notify={notify}
@@ -106,7 +95,6 @@ class Stream extends React.Component {
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={topLevelComment.id}
commentIsIgnored={this.commentIsIgnored}
comment={topLevelComment}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
@@ -133,7 +121,6 @@ class Stream extends React.Component {
postDontAgree,
deleteAction,
showSignInDialog,
ignoreUser,
activeStreamTab,
setActiveStreamTab,
loadNewReplies,
@@ -183,7 +170,6 @@ class Stream extends React.Component {
root={root}
comments={comments}
commentClassNames={commentClassNames}
ignoreUser={ignoreUser}
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
notify={notify}
@@ -197,7 +183,6 @@ class Stream extends React.Component {
loadNewReplies={loadNewReplies}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
commentIsIgnored={this.commentIsIgnored}
charCountEnable={asset.settings.charCountEnable}
maxCharCount={asset.settings.charCount}
editComment={editComment}
@@ -322,9 +307,6 @@ Stream.propTypes = {
notify: PropTypes.func.isRequired,
postComment: PropTypes.func.isRequired,
// dispatch action to ignore another user
ignoreUser: React.PropTypes.func,
// edit a comment, passed (id, asset_id, { body })
editComment: React.PropTypes.func
};
@@ -1,60 +0,0 @@
import React, {PropTypes} from 'react';
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>
);
}
}
@@ -85,13 +85,24 @@ const singleCommentFragment = gql`
edited
editableUntil
}
${getSlotFragmentSpreads(slots, 'comment')}
}
`;
const withCommentFragments = withFragments({
root: gql`
fragment CoralEmbedStream_Comment_root on RootQuery {
me {
ignoredUsers {
id
}
}
__typename
me {
ignoredUsers {
id
}
}
${getSlotFragmentSpreads(slots, 'root')}
}
`,
@@ -116,7 +127,6 @@ const withCommentFragments = withFragments({
endCursor
}
`, THREADING_LEVEL)}
${getSlotFragmentSpreads(slots, 'comment')}
}
${singleCommentFragment}
`
@@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
import {ADDTL_COMMENTS_ON_LOAD_MORE, THREADING_LEVEL} from '../constants/stream';
import {
withPostComment, withPostFlag, withPostDontAgree,
withDeleteAction, withIgnoreUser, withEditComment
withDeleteAction, withEditComment
} from 'coral-framework/graphql/mutations';
import * as authActions from 'coral-embed-stream/src/actions/auth';
@@ -374,7 +374,6 @@ export default compose(
withPostComment,
withPostFlag,
withPostDontAgree,
withIgnoreUser,
withDeleteAction,
withEditComment,
)(StreamContainer);
@@ -108,33 +108,6 @@ export default {
`,
},
mutations: {
IgnoreUser: ({variables}) => ({
updateQueries: {
CoralEmbedStream_Embed: (previousData) => {
const ignoredUserId = variables.id;
const updated = update(previousData, {me: {ignoredUsers: {$push: [{
id: ignoredUserId,
__typename: 'User',
}]}}});
return updated;
}
}
}),
StopIgnoringUser: ({variables}) => ({
updateQueries: {
CoralEmbedStream_Profile: (previousData) => {
const noLongerIgnoredUserId = variables.id;
// remove noLongerIgnoredUserId from ignoredUsers
const updated = update(previousData, {me: {ignoredUsers: {
$apply: (ignoredUsers) => {
return ignoredUsers.filter((u) => u.id !== noLongerIgnoredUserId);
}
}}});
return updated;
}
}
}),
PostComment: ({
variables: {comment: {asset_id, body, parent_id, tags = []}},
state: {auth},
+1 -1
View File
@@ -38,7 +38,7 @@ export default class Snackbar {
}
alert(message) {
const [type, text] = message.split('|');
const {type, text} = JSON.parse(message);
this.el.style.transform = 'translate(-50%, 20px)';
this.el.style.opacity = 0;
this.el.className = `coral-notif-${type}`;
+1 -1
View File
@@ -293,7 +293,7 @@ export const withIgnoreUser = withMutation(
}
`, {
props: ({mutate}) => ({
ignoreUser: ({id}) => {
ignoreUser: (id) => {
return mutate({
variables: {
id,
@@ -5,14 +5,14 @@
*/
export function createNotificationService(pym) {
return {
success(msg) {
pym.sendMessage('coral-alert', `success|${msg}`);
success(text) {
pym.sendMessage('coral-alert', JSON.stringify({kind: 'success', text}));
},
error(msg) {
pym.sendMessage('coral-alert', `error|${msg}`);
error(text) {
pym.sendMessage('coral-alert', JSON.stringify({kind: 'error', text}));
},
info(msg) {
pym.sendMessage('coral-alert', `info|${msg}`);
info(text) {
pym.sendMessage('coral-alert', JSON.stringify({kind: 'info', text}));
},
};
}
@@ -1,24 +0,0 @@
.ignoredUser {
display: table-row;
}
.ignoredUserList {
display: table;
}
.ignoredUser > * {
display: table-cell;
}
.stopListening {
color: #D0011B;
}
.link {
text-decoration: underline;
cursor: pointer;
}
.stopListening:before {
content: '\00a0\00a0\00a0\00a0';
}
@@ -1,43 +0,0 @@
import React, {Component, PropTypes} from 'react';
import t from 'coral-framework/services/i18n';
import styles from './IgnoredUsers.css';
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;
@@ -3,12 +3,10 @@ import {compose, gql} from 'react-apollo';
import React, {Component} from 'react';
import {bindActionCreators} from 'redux';
import {withQuery} from 'coral-framework/hocs';
import {withStopIgnoringUser} from 'coral-framework/graphql/mutations';
import Slot from 'coral-framework/components/Slot';
import {link} from 'coral-framework/services/pym';
import NotLoggedIn from '../components/NotLoggedIn';
import IgnoredUsers from '../components/IgnoredUsers';
import {Spinner} from 'coral-ui';
import CommentHistory from 'talk-plugin-history/CommentHistory';
@@ -55,7 +53,7 @@ class ProfileContainer extends Component {
};
render() {
const {auth, auth: {user}, showSignInDialog, stopIgnoringUser, root, data} = this.props;
const {auth, auth: {user}, showSignInDialog, root, data} = this.props;
const {me} = this.props.root;
const loading = this.props.data.loading;
@@ -77,15 +75,11 @@ class ProfileContainer extends Component {
<h2>{user.username}</h2>
{emailAddress ? <p>{emailAddress}</p> : null}
{me.ignoredUsers && me.ignoredUsers.length
? <div>
<h3>{t('framework.ignored_users')}</h3>
<IgnoredUsers
users={me.ignoredUsers}
stopIgnoring={stopIgnoringUser}
/>
</div>
: null}
<Slot
fill="profileSections"
data={data}
queryData={{root}}
/>
<hr />
@@ -98,8 +92,10 @@ class ProfileContainer extends Component {
}
}
// TODO: This Slot should be included in `talk-plugin-history` instead.
const slots = [
'profileSections',
// TODO: This Slot should be included in `talk-plugin-history` instead.
'commentContent',
];
@@ -143,10 +139,6 @@ const withProfileQuery = withQuery(
query CoralEmbedStream_Profile {
me {
id
ignoredUsers {
id,
username,
}
comments(query: {limit: 10}) {
...TalkSettings_CommentConnectionFragment
}
@@ -165,6 +157,5 @@ const mapDispatchToProps = (dispatch) =>
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withStopIgnoringUser,
withProfileQuery
)(ProfileContainer);
-3
View File
@@ -219,7 +219,6 @@ en:
framework:
banned_account_header: "Your account is currently banned."
banned_account_body: "This means that you cannot Like, Report, or write comments."
because_you_ignored: "Because you ignored the following commenters, their comments are hidden."
comment: comment
comment_is_ignored: "This comment is hidden because you ignored this user."
comments: comments
@@ -230,13 +229,11 @@ en:
error: "Usernames can contain letters numbers and _ only"
label: "New Username"
msg: "Your account is currently suspended because your username has been deemed inappropriate. To restore your account please enter a new username. Please contact us if you have any questions."
ignored_users: "Ignored users"
my_comments: "My Comments"
my_profile: "My profile"
new_count: "View {0} new {1}"
profile: Profile
show_all_comments: "Show all comments"
stop_ignoring: "Stop ignoring"
success_bio_update: "Your biography has been updated"
success_name_update: "Your username has been updated"
success_update_settings: "The changes you have made have been applied to the comment stream on this article"
-2
View File
@@ -227,13 +227,11 @@ es:
error: "Nombres de usuarios pueden solamente incluir letras, números y _"
label: "Nuevo Nombre"
msg: "Tu cuenta está suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario. Contáctanos si tienes alguna pregunta."
ignored_users: "Usuarios ignorados"
my_comments: "Mis Comentarios"
my_profile: "Mi perfil"
new_count: "Ver {0} {1} nuevo"
profile: Perfil
show_all_comments: "Mostrar todos los comentarios"
stop_ignoring: "No ignorar más"
success_bio_update: "Tu biografia fue actualizada"
success_name_update: "Tu nombre de usuario ha sido actualizado"
success_update_settings: "La configuración de este articulo fue actualizada"
-1
View File
@@ -188,7 +188,6 @@ fr:
error: "Les noms d'utilisateur ne peuvent contenir que des chiffres, des lettres et \"_\""
label: "Nouveau nom d'utilisateur"
msg: "Votre compte est actuellement suspendu car votre nom d'utilisateur a été jugé inapproprié. Pour restaurer votre compte, entrez un nouveau nom d'utilisateur. Contactez-nous si vous avez des questions."
ignored_users: "Utilisateurs ignorés"
my_comments: "Mes commentaires"
my_profile: "Mon profil"
new_count: "Voir {0} nouveau {1}"
-3
View File
@@ -214,7 +214,6 @@ pt_BR:
framework:
banned_account_header: "Sua conta está atualmente proibida."
banned_account_body: "Isso significa que você não pode gostar, informar ou escrever comentários."
because_you_ignored: "Porque você ignorou os seguintes comentadores, seus comentários estão ocultos."
comment: comentário
comment_is_ignored: "Este comentário está oculto porque você ignorou esse usuário."
comments: comentários
@@ -225,13 +224,11 @@ pt_BR:
error: "Nomes de usuários podem conter números de letras e _ somente"
label: "Novo usuário"
msg: "Sua conta está suspensa porque seu nome de usuário foi considerado inapropriado. Para restaurar sua conta, insira um novo nome de usuário. Entre em contato conosco se você tiver alguma dúvida."
ignored_users: "Usuários ignorados"
my_comments: "Meus comentários"
my_profile: "Meu perfil"
new_count: "Ver {0} {1}"
profile: Perfil
show_all_comments: "Exibir todos os comentários"
stop_ignoring: "Pare de ignorar"
success_bio_update: "Sua biografia foi atualizada"
success_name_update: "Seu nome de usuário foi atualizado"
success_update_settings: "As alterações que você fez foram aplicadas no hilo de comentários neste artigo"
+3 -2
View File
@@ -104,7 +104,7 @@
"graphql": "^0.9.1",
"graphql-docs": "^0.2.0",
"graphql-errors": "^2.1.0",
"graphql-redis-subscriptions": "^1.1.5",
"graphql-redis-subscriptions": "^1.3.0",
"graphql-server-express": "^0.6.0",
"graphql-subscriptions": "^0.4.3",
"graphql-tag": "^1.2.3",
@@ -115,6 +115,7 @@
"immutability-helper": "^2.2.0",
"imports-loader": "^0.7.1",
"inquirer": "^3.2.2",
"ioredis": "^3.1.4",
"istanbul": "^1.1.0-alpha.1",
"joi": "^10.6.0",
"json-loader": "^0.5.7",
@@ -129,6 +130,7 @@
"material-design-lite": "^1.2.1",
"metascraper": "^1.0.7",
"minimist": "^1.2.0",
"moment": "^2.18.1",
"mongoose": "^4.11.7",
"morgan": "^1.8.2",
"ms": "^2.0.0",
@@ -163,7 +165,6 @@
"react-toastify": "^1.5.0",
"react-transition-group": "^1.1.3",
"recompose": "^0.23.1",
"redis": "^2.8.0",
"redux": "^3.6.0",
"redux-thunk": "^2.1.0",
"resolve": "^1.4.0",
@@ -1 +1 @@
export {addNotification} from 'coral-framework/actions/notification';
export {notify} from 'coral-framework/actions/notification';
+4
View File
@@ -5,3 +5,7 @@ export {default as withFragments} from 'coral-framework/hocs/withFragments';
export {default as excludeIf} from 'coral-framework/hocs/excludeIf';
export {default as connect} from 'coral-framework/hocs/connect';
export {default as withEmit} from 'coral-framework/hocs/withEmit';
export {
withIgnoreUser,
withStopIgnoringUser,
} from 'coral-framework/graphql/mutations';
+1
View File
@@ -7,4 +7,5 @@ export {
capitalize,
getErrorMessages,
getDefinitionName,
getShallowChanges,
} from 'coral-framework/utils';
+4 -1
View File
@@ -17,6 +17,9 @@
"talk-plugin-sort-newest",
"talk-plugin-sort-oldest",
"talk-plugin-sort-most-respected",
"talk-plugin-sort-most-replied"
"talk-plugin-sort-most-replied",
"talk-plugin-author-menu",
"talk-plugin-member-since",
"talk-plugin-ignore-user"
]
}
@@ -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,19 @@
import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT, OPEN_MENU, CLOSE_MENU} from './constants';
export const setContentSlot = (slot) => ({
type: SET_CONTENT_SLOT,
slot,
});
export const resetContentSlot = () => ({
type: RESET_CONTENT_SLOT,
});
export const openMenu = (id) => ({
type: OPEN_MENU,
id,
});
export const closeMenu = () => ({
type: CLOSE_MENU,
});
@@ -0,0 +1,16 @@
.root {
display: inline-block;
position: relative;
}
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
&:hover {
text-decoration: underline;
}
}
.name {
font-weight: bold;
}
@@ -0,0 +1,31 @@
import React from 'react';
import Menu from './Menu';
import styles from './AuthorName.css';
import {ClickOutside} from 'plugin-api/beta/client/components';
import cn from 'classnames';
export default ({data, root, asset, comment, contentSlot, menuVisible, toggleMenu, hideMenu}) => {
return (
<ClickOutside onClickOutside={hideMenu}>
<div className={cn(styles.root, 'talk-plugin-author-menu')}>
<button
className={cn(styles.button, 'talk-plugin-author-menu-button')}
onClick={toggleMenu}
>
<span className={styles.name}>
{comment.user.username}
</span>
</button>
{menuVisible &&
<Menu
data={data}
root={root}
asset={asset}
comment={comment}
contentSlot={contentSlot}
/>
}
</div>
</ClickOutside>
);
};
@@ -0,0 +1,45 @@
.menu {
background-color: white;
border: solid 1px #999;
border-radius: 3px;
padding: 10px;
position: absolute;
-webkit-box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
z-index: 9;
top: 26px;
left: 0px;
min-width: 20px;
text-align: left;
}
.menu::before{
content: '';
border: 10px solid transparent;
border-top-color: #999;
position: absolute;
left: 6px;
top: -21px;
transform: rotate(180deg);
}
.menu::after{
content: '';
border: 10px solid transparent;
border-top-color: white;
position: absolute;
left: 6px;
top: -20px;
transform: rotate(180deg);
}
.actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
&:empty {
display: none;
}
}
@@ -0,0 +1,35 @@
import React from 'react';
import styles from './Menu.css';
import {Slot} from 'plugin-api/beta/client/components';
import cn from 'classnames';
export default ({data, root, asset, comment, contentSlot}) => {
if (contentSlot) {
return (
<div className={cn(styles.menu, 'talk-plugin-author-menu-popup')}>
<Slot
fill={contentSlot}
data={data}
queryData={{asset, root, comment}}
/>
</div>
);
}
return (
<div className={cn(styles.menu, 'talk-plugin-author-menu-popup')}>
<Slot
className={cn('talk-plugin-author-menu-infos')}
fill={'authorMenuInfos'}
data={data}
queryData={{asset, root, comment}}
/>
<Slot
className={cn(styles.actions, 'talk-plugin-author-menu-actions')}
fill={'authorMenuActions'}
data={data}
queryData={{asset, root, comment}}
/>
</div>
);
};
@@ -0,0 +1,7 @@
const prefix = 'TALK_AUTHOR_MENU';
export const SET_CONTENT_SLOT = `${prefix}_SET_CONTENT_SLOT`;
export const RESET_CONTENT_SLOT = `${prefix}_RESET_CONTENT_SLOT`;
export const OPEN_MENU = `${prefix}_OPEN_MENU`;
export const CLOSE_MENU = `${prefix}_CLOSE_MENU`;
@@ -0,0 +1,102 @@
import React from 'react';
import {connect, withFragments} from 'plugin-api/beta/client/hocs';
import {bindActionCreators} from 'redux';
import AuthorName from '../components/AuthorName';
import {setContentSlot, resetContentSlot, openMenu, closeMenu} from '../actions';
import {compose, gql} from 'react-apollo';
import {getSlotFragmentSpreads, getShallowChanges} from 'plugin-api/beta/client/utils';
class AuthorNameContainer extends React.Component {
shouldComponentUpdate(nextProps) {
// Specifically handle `showMenuForComment` if it is the only change.
const changes = getShallowChanges(this.props, nextProps);
if (changes.length === 1 && changes[0] === 'showMenuForComment') {
const commentId = this.props.comment.id;
if (
commentId !== this.props.showMenuForComment &&
commentId !== nextProps.showMenuForComment
) {
return false;
}
}
// Prevent Slot from rerendering when no props has shallowly changed.
return changes.length !== 0;
}
toggleMenu = () => {
if (this.props.showMenuForComment === this.props.comment.id) {
this.props.closeMenu();
} else {
this.props.openMenu(this.props.comment.id);
}
}
hideMenu = () => {
if (this.props.showMenuForComment === this.props.comment.id) {
this.props.closeMenu();
}
}
render() {
return <AuthorName
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
comment={this.props.comment}
contentSlot={this.props.contentSlot}
menuVisible={this.props.showMenuForComment === this.props.comment.id}
toggleMenu={this.toggleMenu}
hideMenu={this.hideMenu}
/>;
}
}
const slots = [
'authorMenuInfos',
'authorMenuActions',
];
const mapStateToProps = ({talkPluginAuthorMenu: state}) => ({
contentSlot: state.contentSlot,
showMenuForComment: state.showMenuForComment,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
setContentSlot,
resetContentSlot,
openMenu,
closeMenu,
}, dispatch);
const withAuthorNameFragments = withFragments({
root: gql`
fragment TalkAuthorMenu_AuthorName_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}`,
asset: gql`
fragment TalkAuthorMenu_AuthorName_asset on Asset {
__typename
${getSlotFragmentSpreads(slots, 'asset')}
}`,
comment: gql`
fragment TalkAuthorMenu_AuthorName_comment on Comment {
__typename
id
user {
username
}
${getSlotFragmentSpreads(slots, 'comment')}
}`,
});
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withAuthorNameFragments,
);
export default enhance(AuthorNameContainer);
@@ -0,0 +1,11 @@
import AuthorName from './containers/AuthorName';
import reducer from './reducer';
import translations from './translations.yml';
export default {
reducer,
slots: {
commentAuthorName: [AuthorName]
},
translations
};
@@ -0,0 +1,34 @@
import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT, OPEN_MENU, CLOSE_MENU} from './constants';
const initialState = {
contentSlot: null,
showMenuForComment: null,
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case SET_CONTENT_SLOT:
return {
...state,
contentSlot: action.slot,
};
case RESET_CONTENT_SLOT:
return {
...state,
contentSlot: initialState.contentSlot,
};
case OPEN_MENU:
return {
...state,
showMenuForComment: action.id,
};
case CLOSE_MENU:
return {
...state,
showMenuForComment: null,
contentSlot: null,
};
default :
return state;
}
}
@@ -0,0 +1,4 @@
en:
talk-plugin-author-menu:
es:
talk-plugin-author-menu:
+1
View File
@@ -0,0 +1 @@
module.exports = {};
@@ -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,17 @@
.root {
white-space: nowrap;
}
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
border: 1px solid rgba(1, 1, 1, 0.8);
border-radius: 1px;
padding: 3px 6px;
font-size: 12px;
transition: color 100ms, background 100ms;
&:hover {
background-color: rgba(1, 1, 1, 0.8);
color: white;
}
}
@@ -0,0 +1,12 @@
import React from 'react';
import styles from './IgnoreUserAction.css';
import {t} from 'plugin-api/beta/client/services';
import cn from 'classnames';
export default ({ignoreUser}) => (
<button
className={cn(styles.button, 'talk-plugin-ignore-user-action')}
onClick={ignoreUser}>
{t('talk-plugin-ignore-user.ignore_user')}
</button>
);
@@ -0,0 +1,45 @@
.root {
width: 200px;
}
.actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.message {
font-size: 13px;
}
.title {
padding: 0;
margin: 0 0 8px 0;
font-size: 15px;
}
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
border: 1px solid rgba(1, 1, 1, 0.8);
border-radius: 1px;
padding: 3px 6px;
font-size: 12px;
transition: color 100ms, background 100ms;
&:hover {
background-color: rgba(1, 1, 1, 0.8);
color: white;
}
}
.cancel {
composes: buttonReset from "coral-framework/styles/reset.css";
padding: 3px 6px;
font-size: 12px;
transition: color 100ms;
margin-right: 4px;
&:hover {
color: rgba(1, 1, 1, 0.75);
}
}
@@ -0,0 +1,23 @@
import React from 'react';
import styles from './IgnoreUserConfirmation.css';
import {t} from 'plugin-api/beta/client/services';
import cn from 'classnames';
export default ({ignoreUser, cancel, username}) => (
<aside className={cn(styles.root, 'talk-plugin-ignore-user-confirmation')}>
<div className={styles.message}>
<h1 className={styles.title}>
{t('talk-plugin-ignore-user.confirmation_title', username)}
</h1>
{t('talk-plugin-ignore-user.confirmation')}
</div>
<div className={cn(styles.actions, 'talk-plugin-ignore-user-confirmation-actions')}>
<button className={cn(styles.cancel, 'talk-plugin-ignore-user-confirmation-cancel')} onClick={cancel}>
{t('talk-plugin-ignore-user.cancel')}
</button>
<button className={cn(styles.button, 'talk-plugin-ignore-user-confirmation-button')} onClick={ignoreUser}>
{t('talk-plugin-ignore-user.ignore_user')}
</button>
</div>
</aside>
);
@@ -0,0 +1,22 @@
.list {
display: table;
list-style-type: none;
margin-left: 0;
padding-left: 0;
}
.listItem {
display: table-row;
}
.username {
display: table-cell;
}
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
margin-left: 16px;
color: #D0011B;
text-decoration: underline;
}
@@ -0,0 +1,23 @@
import React from 'react';
import styles from './IgnoredUserSection.css';
import {t} from 'plugin-api/beta/client/services';
export default ({ignoredUsers, stopIgnoringUser}) => (
<section className={'talk-plugin-ignore-user-section'}>
<h3>{t('talk-plugin-ignore-user.section_title')}</h3>
<p>{t('talk-plugin-ignore-user.section_info')}</p>
<ul className={styles.list}>
{ignoredUsers.map(({username, id}) => (
<li className={styles.listItem} key={id}>
<span className={styles.username}>{username}</span>
<button
onClick={() => stopIgnoringUser({id})}
className={styles.button}
>
{t('talk-plugin-ignore-user.stop_ignoring')}
</button>
</li>
))}
</ul>
</section>
);
@@ -0,0 +1,53 @@
import React from 'react';
import IgnoreUserAction from '../components/IgnoreUserAction';
import {compose, gql} from 'react-apollo';
import {connect, withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
import {bindActionCreators} from 'redux';
import {setContentSlot} from 'plugins/talk-plugin-author-menu/client/actions';
import IgnoreUserConfirmation from './IgnoreUserConfirmation';
import {getDefinitionName} from 'plugin-api/beta/client/utils';
class IgnoreUserActionContainer extends React.Component {
ignoreUser = () => {
this.props.setContentSlot('ignoreUserConfirmation');
};
render() {
return <IgnoreUserAction
ignoreUser={this.ignoreUser}
/>;
}
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
setContentSlot,
}, dispatch);
const withIgnoreUserActionFragments = withFragments({
root: gql`
fragment TalkIgnoreUser_IgnoreUserAction_root on RootQuery {
me {
id
}
}
`,
comment: gql`
fragment TalkIgnoreUser_IgnoreUserAction_comment on Comment {
user {
id
}
...${getDefinitionName(IgnoreUserConfirmation.fragments.comment)}
}
${IgnoreUserConfirmation.fragments.comment}
`,
});
const enhance = compose(
connect(null, mapDispatchToProps),
withIgnoreUserActionFragments,
excludeIf(({root: {me}, comment}) => !me || me.id === comment.user.id),
);
export default enhance(IgnoreUserActionContainer);
@@ -0,0 +1,60 @@
import React from 'react';
import IgnoreUserConfirmation from '../components/IgnoreUserConfirmation';
import {compose, gql} from 'react-apollo';
import {connect, withFragments, withIgnoreUser} from 'plugin-api/beta/client/hocs';
import {bindActionCreators} from 'redux';
import {closeMenu} from 'plugins/talk-plugin-author-menu/client/actions';
import {notify} from 'plugin-api/beta/client/actions/notification';
import {t} from 'plugin-api/beta/client/services';
import {getErrorMessages} from 'plugin-api/beta/client/utils';
class IgnoreUserConfirmationContainer extends React.Component {
ignoreUser = () => {
const {ignoreUser, notify, comment, closeMenu} = this.props;
ignoreUser(comment.user.id)
.then(() => {
notify('success', t('talk-plugin-ignore-user.notify_success', comment.user.username));
})
.catch((err) => {
notify('error', getErrorMessages(err));
});
closeMenu();
};
cancel = () => {
this.props.closeMenu();
}
render() {
return <IgnoreUserConfirmation
username={this.props.comment.user.username}
ignoreUser={this.ignoreUser}
cancel={this.cancel}
/>;
}
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
closeMenu,
notify,
}, dispatch);
const withIgnoreUserConfirmationFragments = withFragments({
comment: gql`
fragment TalkIgnoreUser_IgnoreUserConfirmation_comment on Comment {
user {
id
username
}
}`,
});
const enhance = compose(
connect(null, mapDispatchToProps),
withIgnoreUserConfirmationFragments,
withIgnoreUser,
);
export default enhance(IgnoreUserConfirmationContainer);
@@ -0,0 +1,36 @@
import React from 'react';
import IgnoredUserSection from '../components/IgnoredUserSection';
import {compose, gql} from 'react-apollo';
import {withFragments, excludeIf, withStopIgnoringUser} from 'plugin-api/beta/client/hocs';
class IgnoredUserSectionContainer extends React.Component {
render() {
return <IgnoredUserSection
stopIgnoringUser={this.props.stopIgnoringUser}
ignoredUsers={this.props.root.me.ignoredUsers}
/>;
}
}
const withIgnoredUserSectionFragments = withFragments({
root: gql`
fragment TalkIgnoreUser_IgnoredUserSection_root on RootQuery {
me {
id
ignoredUsers {
id,
username,
}
}
}
`,
});
const enhance = compose(
withIgnoredUserSectionFragments,
withStopIgnoringUser,
excludeIf(({root: {me}}) => me.ignoredUsers.length === 0),
);
export default enhance(IgnoredUserSectionContainer);
@@ -0,0 +1,43 @@
import IgnoreUserAction from './containers/IgnoreUserAction';
import IgnoreUserConfirmation from './containers/IgnoreUserConfirmation';
import IgnoredUserSection from './containers/IgnoredUserSection';
import translations from './translations.yml';
import update from 'immutability-helper';
export default {
slots: {
authorMenuActions: [IgnoreUserAction],
ignoreUserConfirmation: [IgnoreUserConfirmation],
profileSections: [IgnoredUserSection],
},
translations,
mutations: {
IgnoreUser: ({variables}) => ({
updateQueries: {
CoralEmbedStream_Embed: (previousData) => {
const ignoredUserId = variables.id;
const updated = update(previousData, {me: {ignoredUsers: {$push: [{
id: ignoredUserId,
__typename: 'User',
}]}}});
return updated;
}
}
}),
StopIgnoringUser: ({variables}) => ({
updateQueries: {
CoralEmbedStream_Profile: (previousData) => {
const noLongerIgnoredUserId = variables.id;
// remove noLongerIgnoredUserId from ignoredUsers
const updated = update(previousData, {me: {ignoredUsers: {
$apply: (ignoredUsers) => {
return ignoredUsers.filter((u) => u.id !== noLongerIgnoredUserId);
}
}}});
return updated;
}
}
}),
},
};
@@ -0,0 +1,25 @@
en:
talk-plugin-ignore-user:
section_title: Ignored users
section_info: Because you ignored the following commenters, their comments are hidden.
stop_ignoring: Stop ignoring
ignore_user: Ignore User
cancel: Cancel
confirmation: |
When you ignore a user, all comments they wrote on the site will be hidden from you. You can
undo this later from My Profile.
notify_success: |
You are now ignoring {0}. You can undo this action from My Profile.
confirmation_title: Ignore {0}?
es:
talk-plugin-ignore-user:
section_title: "Usuarios ignorados"
stop_ignoring: "No ignorar más"
fr:
talk-plugin-ignore-user:
section_title: "Utilisateurs ignorés"
pt_Br:
talk-plugin-ignore-user:
section_title: "Usuários ignorados"
section_info: "Porque você ignorou os seguintes comentadores, seus comentários estão ocultos."
stop_ignoring: "Pare de ignorar"
+1
View File
@@ -0,0 +1 @@
module.exports = {};
+1 -1
View File
@@ -5,5 +5,5 @@ export default {
translations,
slots: {
commentReactions: [LikeButton]
}
},
};
@@ -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,19 @@
.root {
white-space: nowrap;
}
.root:not(:last-child) {
margin-bottom: 4px;
}
.memberSince {
margin-left: 4px;
letter-spacing: 0.2px;
font-size: 13px;
vertical-align: middle;
}
.icon {
font-size: 18px;
vertical-align: middle;
}
@@ -0,0 +1,15 @@
import React from 'react';
import styles from './MemberSinceInfo.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import cn from 'classnames';
import moment from 'moment';
export default ({memberSinceDate}) => (
<div className={cn(styles.root, 'talk-plugin-member-since')}>
<Icon name="date_range" className={cn(styles.icon, 'talk-plugin-member-since-icon')} />
<span className={cn(styles.memberSince, 'talk-plugin-member-since-date')}>
{t('talk-plugin-member-since.member_since')}: {moment(new Date(memberSinceDate)).format('MMM DD, YYYY')}
</span>
</div>
);
@@ -0,0 +1,28 @@
import React from 'react';
import MemberSinceInfo from '../components/MemberSinceInfo';
import {compose, gql} from 'react-apollo';
import {withFragments} from 'plugin-api/beta/client/hocs';
class MemberSinceInfoContainer extends React.Component {
render() {
return <MemberSinceInfo
memberSinceDate={this.props.comment.user.created_at}
/>;
}
}
const withMemberSinceInfoFragments = withFragments({
comment: gql`
fragment TalkAuthorMenu_MemberSinceInfo_comment on Comment {
user {
username
created_at
}
}`,
});
const enhance = compose(
withMemberSinceInfoFragments,
);
export default enhance(MemberSinceInfoContainer);
@@ -0,0 +1,9 @@
import MemberSinceInfo from './containers/MemberSinceInfo';
import translations from './translations.yml';
export default {
slots: {
authorMenuInfos: [MemberSinceInfo]
},
translations
};
@@ -0,0 +1,6 @@
en:
talk-plugin-member-since:
member_since: "Member Since"
es:
talk-plugin-member-since:
member_since: "Miembro desde"
@@ -0,0 +1 @@
module.exports = {};
+64 -180
View File
@@ -1,6 +1,5 @@
const redis = require('./redis');
const debug = require('debug')('talk:services:cache');
const crypto = require('crypto');
const cache = module.exports = {};
@@ -52,60 +51,6 @@ cache.wrap = async (key, expiry, work, kf = keyfunc) => {
return value;
};
// This is designed to increment a key and add an expiry iff the key already
// exists.
const INCR_SCRIPT = `
if redis.call('GET', KEYS[1]) ~= false then
redis.call('INCR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
`;
// This is designed to decrement a key and add an expiry iff the key already
// exists.
const DECR_SCRIPT = `
if redis.call('GET', KEYS[1]) ~= false then
redis.call('DECR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
`;
// Load the script into redis and track the script hash that we will use to exec
// increments on.
const loadScript = (name, script) => new Promise((resolve, reject) => {
let shasum = crypto.createHash('sha1');
shasum.update(script);
let hash = shasum.digest('hex');
cache.client
.script('EXISTS', hash, (err, [exists]) => {
if (err) {
return reject(err);
}
if (exists) {
debug(`already loaded ${name} as SHA[${hash}], not loading again`);
return resolve(hash);
}
debug(`${name} not loaded as SHA[${hash}], loading`);
cache.client
.script('load', script, (err, hash) => {
if (err) {
return reject(err);
}
debug(`loaded ${name} as SHA[${hash}]`);
resolve(hash);
});
});
});
/**
* Init sets up the scripts used in Redis with the incr/decr commands.
*/
@@ -114,93 +59,77 @@ cache.init = async () => {
// Create the redis instance.
cache.client = redis.createClient();
// Load the INCR_SCRIPT and DECR_SCRIPT into Redis.
let [incrScriptHash, decrScriptHash] = await Promise.all([
loadScript('INCR_SCRIPT', INCR_SCRIPT),
loadScript('DECR_SCRIPT', DECR_SCRIPT)
]);
// This is designed to increment a key and add an expiry iff the key already
// exists.
const INCR_SCRIPT = `
if redis.call('GET', KEYS[1]) ~= false then
redis.call('INCR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
`;
// Set the globally scoped cache hashes.
cache.INCR_SCRIPT_HASH = incrScriptHash;
cache.DECR_SCRIPT_HASH = decrScriptHash;
cache.client.defineCommand('increx', {
numberOfKeys: 1,
lua: INCR_SCRIPT,
});
// This is designed to decrement a key and add an expiry iff the key already
// exists.
const DECR_SCRIPT = `
if redis.call('GET', KEYS[1]) ~= false then
redis.call('DECR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
`;
cache.client.defineCommand('decrex', {
numberOfKeys: 1,
lua: DECR_SCRIPT,
});
};
/**
* This will increment a key in redis and update the expiry iff it already
* exists, otherwise it will do nothing.
*/
cache.incr = (key, expiry, kf = keyfunc) => new Promise((resolve, reject) => {
cache.client
.evalsha(cache.INCR_SCRIPT_HASH, 1, kf(key), expiry, (err) => {
if (err) {
return reject(err);
}
return resolve();
});
});
cache.incr = async (key, expiry, kf = keyfunc) => cache.client.increx(kf(key), expiry);
/**
* This will decrement a key in redis and update the expiry iff it already
* exists, otherwise it will do nothing.
*/
cache.decr = (key, expiry, kf = keyfunc) => new Promise((resolve, reject) => {
cache.client
.evalsha(cache.DECR_SCRIPT_HASH, 1, kf(key), expiry, (err) => {
if (err) {
return reject(err);
}
return resolve();
});
});
cache.decr = async (key, expiry, kf = keyfunc) => cache.client.decrex(kf(key, expiry));
/**
* This will increment many keys in redis and update the expiry iff it already
* exists, otherwise it will do nothing.
*/
cache.incrMany = (keys, expiry, kf = keyfunc) => {
cache.incrMany = async (keys, expiry, kf = keyfunc) => {
let multi = cache.client.multi();
keys.forEach((key) => {
for (const key of keys) {
// Queue up the evalsha command.
multi.evalsha(cache.INCR_SCRIPT_HASH, 1, kf(key), expiry);
});
multi.increx(kf(key), expiry);
}
return new Promise((resolve, reject) => {
multi.exec((err) => {
if (err) {
return reject(err);
}
resolve();
});
});
return multi.exec();
};
/**
* This will decrement many keys in redis and update the expiry iff it already
* exists, otherwise it will do nothing.
*/
cache.decrMany = (keys, expiry, kf = keyfunc) => {
cache.decrMany = async (keys, expiry, kf = keyfunc) => {
let multi = cache.client.multi();
keys.forEach((key) => {
for (const key of keys) {
// Queue up the evalsha command.
multi.evalsha(cache.DECR_SCRIPT_HASH, 1, kf(key), expiry);
});
multi.decrex(kf(key), expiry);
}
return new Promise((resolve, reject) => {
multi.exec((err) => {
if (err) {
return reject(err);
}
resolve();
});
});
return multi.exec();
};
/**
@@ -257,28 +186,12 @@ cache.wrapMany = async (keys, expiry, work, kf = keyfunc) => {
* @param {Mixed} key Either an array of items composing a key or a string
* @return {Promise}
*/
cache.get = (key, kf = keyfunc) => new Promise((resolve, reject) => {
cache.client.get(kf(key), (err, reply) => {
if (err) {
return reject(err);
}
cache.get = async (key, kf = keyfunc) => cache.client.get(kf(key)).then((reply) => {
if (reply !== null) {
if (reply !== null) {
let value;
try {
// Parse the stored cache value from JSON.
value = JSON.parse(reply);
} catch (e) {
return reject(e);
}
return resolve(value);
}
resolve(null);
});
// Parse the stored cache value from JSON.
return JSON.parse(reply);
}
});
/**
@@ -288,31 +201,22 @@ cache.get = (key, kf = keyfunc) => new Promise((resolve, reject) => {
* @param {Function} [kf=keyfunc] optional key function to use to turn the
* provided key into a string for the cache.
*/
cache.getMany = (keys, kf = keyfunc) => new Promise((resolve, reject) => {
cache.client.mget(keys.map(kf), (err, replies) => {
if (err) {
return reject(err);
cache.getMany = async (keys, kf = keyfunc) => cache.client.mget(keys.map(kf)).then((replies) => {
// Parse the replies.
for (let i = 0; i < replies.length; i++) {
let value = null;
if (replies[i] != null) {
// Parse the stored cache value from JSON.
value = JSON.parse(replies[i]);
}
// Parse the replies.
for (let i = 0; i < replies.length; i++) {
let value = null;
replies[i] = value;
}
if (replies[i] != null) {
try {
// Parse the stored cache value from JSON.
value = JSON.parse(replies[i]);
} catch (e) {
return reject(e);
}
}
replies[i] = value;
}
return resolve(replies);
});
return replies;
});
/**
@@ -322,7 +226,7 @@ cache.getMany = (keys, kf = keyfunc) => new Promise((resolve, reject) => {
* @param {Function} [kf=keyfunc] optional key function to use to turn the
* provided key into a string for the cache.
*/
cache.setMany = (keys, values, expiry, kf = keyfunc) => {
cache.setMany = async (keys, values, expiry, kf = keyfunc) => {
let multi = cache.client.multi();
keys.forEach((key, index) => {
@@ -334,15 +238,7 @@ cache.setMany = (keys, values, expiry, kf = keyfunc) => {
multi.set(kf(key), reply, 'EX', expiry);
});
return new Promise((resolve, reject) => {
multi.exec((err) => {
if (err) {
return reject(err);
}
resolve();
});
});
return multi.exec();
};
/**
@@ -350,18 +246,12 @@ cache.setMany = (keys, values, expiry, kf = keyfunc) => {
* @param {Mixed} key Either an array of items composing a key or a string
* @return {Promise}
*/
cache.invalidate = (key, kf = keyfunc) => new Promise((resolve, reject) => {
cache.invalidate = async (key, kf = keyfunc) => {
debug(`invalidate: ${kf(key)}`);
cache.client.del(kf(key), (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
return cache.client.del(kf(key));
};
/**
* This sets a value on the key with the expiry and then resolves once it is
@@ -371,16 +261,10 @@ cache.invalidate = (key, kf = keyfunc) => new Promise((resolve, reject) => {
* @param {Integer} expiry Time in seconds for the cache entry to live for
* @return {Promise}
*/
cache.set = (key, value, expiry, kf = keyfunc) => new Promise((resolve, reject) => {
cache.set = async (key, value, expiry, kf = keyfunc) => {
// Serialize the value as JSON.
let reply = JSON.stringify(value);
cache.client.set(kf(key), reply, 'EX', expiry, (err) => {
if (err) {
return reject(err);
}
return resolve();
});
});
return cache.client.set(kf(key), reply, 'EX', expiry);
};
+15 -22
View File
@@ -159,40 +159,33 @@ async function ValidateUserLogin(loginProfile, user, done) {
/**
* Revoke the token on the request.
*/
const HandleLogout = (req, res, next) => {
const HandleLogout = async (req, res, next) => {
const {jwt} = req;
const now = new Date();
const expiry = (jwt.exp - now.getTime() / 1000).toFixed(0);
client().set(`jtir[${jwt.jti}]`, now.toISOString(), 'EX', expiry, (err) => {
if (err) {
return next(err);
}
try {
await client().set(`jtir[${jwt.jti}]`, now.toISOString(), 'EX', expiry);
} catch (err) {
return next(err);
}
// Only clear the cookie on logout if enabled.
if (JWT_CLEAR_COOKIE_LOGOUT) {
debug('clearing the login cookie');
res.clearCookie(JWT_SIGNING_COOKIE_NAME);
}
// Only clear the cookie on logout if enabled.
if (JWT_CLEAR_COOKIE_LOGOUT) {
debug('clearing the login cookie');
res.clearCookie(JWT_SIGNING_COOKIE_NAME);
}
res.status(204).end();
});
res.status(204).end();
};
const checkGeneralTokenBlacklist = (jwt) => new Promise((resolve, reject) => {
client().get(`jtir[${jwt.jti}]`, (err, expiry) => {
if (err) {
return reject(err);
}
const checkGeneralTokenBlacklist = (jwt) => client().get(`jtir[${jwt.jti}]`)
.then((expiry) => {
if (expiry != null) {
return reject(new errors.ErrAuthentication('token was revoked'));
throw new errors.ErrAuthentication('token was revoked');
}
return resolve();
});
});
/**
* Check if the given token is already blacklisted, throw an error if it is.
+6 -6
View File
@@ -1,5 +1,5 @@
const {RedisPubSub} = require('graphql-redis-subscriptions');
const {connectionOptions, attachMonitors} = require('./redis');
const {createClient} = require('./redis');
/**
* getClient returns the pubsub singleton for this instance.
@@ -10,11 +10,11 @@ const getClient = () => {
return pubsub;
}
pubsub = new RedisPubSub({connection: connectionOptions});
// Attach the node monitors to the subscriber + publishers.
attachMonitors(pubsub.redisPublisher);
attachMonitors(pubsub.redisSubscriber);
// Create the new PubSub client, we only need one per instance of Talk.
pubsub = new RedisPubSub({
publisher: createClient(),
subscriber: createClient(),
});
return pubsub;
};
+4 -14
View File
@@ -1,4 +1,4 @@
const redis = require('redis');
const Redis = require('ioredis');
const debug = require('debug')('talk:services:redis');
const enabled = require('debug').enabled('talk:services:redis');
const {
@@ -14,9 +14,10 @@ const attachMonitors = (client) => {
// Debug events.
if (enabled) {
client.on('ready', () => debug('client ready'));
client.on('connect', () => debug('client connected'));
client.on('ready', () => debug('client ready'));
client.on('reconnecting', () => debug('client connection lost, attempting to reconnect'));
client.on('close', () => debug('client closed the connection'));
client.on('end', () => debug('client ended'));
}
@@ -29,7 +30,6 @@ const attachMonitors = (client) => {
};
const connectionOptions = {
url: REDIS_URL,
retry_strategy: function(options) {
if (options.error && options.error.code !== 'ECONNREFUSED') {
@@ -64,25 +64,15 @@ const connectionOptions = {
};
const createClient = () => {
let client = redis.createClient(connectionOptions);
let client = new Redis(REDIS_URL, connectionOptions);
// Attach the monitors that will print debug messages to the console.
attachMonitors(client);
client.ping((err) => {
if (err) {
console.error('Can\'t ping the redis server!');
throw err;
}
});
return client;
};
module.exports = {
connectionOptions,
attachMonitors,
createClient,
createClientFactory: () => {
let client = null;
+104 -138
View File
@@ -69,33 +69,25 @@ module.exports = class UsersService {
* Indicating that the account should be flagged as "login recaptcha required"
* where future login attempts must be made with the recaptcha flag.
*/
static recordLoginAttempt(email) {
static async recordLoginAttempt(email) {
const rdskey = `la[${email.toLowerCase().trim()}]`;
return new Promise((resolve, reject) => {
client()
.multi()
.incr(rdskey)
.expire(rdskey, RECAPTCHA_WINDOW_SECONDS)
.exec((err, replies) => {
if (err) {
return reject(err);
}
const replies = await client()
.multi()
.incr(rdskey)
.expire(rdskey, RECAPTCHA_WINDOW_SECONDS)
.exec();
// if this is new or has no expiry
if (replies[0] === 1 || replies[1] === -1) {
// if this is new or has no expiry
if (replies[0] === 1 || replies[1] === -1) {
// then expire it after the timeout
client().expire(rdskey, RECAPTCHA_WINDOW_SECONDS);
}
// then expire it after the timeout
client().expire(rdskey, RECAPTCHA_WINDOW_SECONDS);
}
if (replies[0] >= RECAPTCHA_INCORRECT_TRIGGER) {
return reject(errors.ErrLoginAttemptMaximumExceeded);
}
resolve();
});
});
if (replies[0] >= RECAPTCHA_INCORRECT_TRIGGER) {
throw errors.ErrLoginAttemptMaximumExceeded;
}
}
/**
@@ -104,27 +96,17 @@ module.exports = class UsersService {
*
* errors.ErrLoginAttemptMaximumExceeded
*/
static checkLoginAttempts(email) {
static async checkLoginAttempts(email) {
const rdskey = `la[${email.toLowerCase().trim()}]`;
return new Promise((resolve, reject) => {
client()
.get(rdskey, (err, reply) => {
if (err) {
return reject(err);
}
const attempts = await client().get(rdskey);
if (!attempts) {
return;
}
if (!reply) {
return resolve();
}
if (reply >= RECAPTCHA_INCORRECT_TRIGGER) {
return reject(errors.ErrLoginAttemptMaximumExceeded);
}
resolve();
});
});
if (attempts >= RECAPTCHA_INCORRECT_TRIGGER) {
throw errors.ErrLoginAttemptMaximumExceeded;
}
}
/**
@@ -217,24 +199,15 @@ module.exports = class UsersService {
});
}
static changePassword(id, password) {
return new Promise((resolve, reject) => {
bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => {
if (err) {
return reject(err);
}
static async changePassword(id, password) {
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
resolve(hashedPassword);
});
})
.then((hashedPassword) => {
return UserModel.update({id}, {
$inc: {__v: 1},
$set: {
password: hashedPassword
}
});
});
return UserModel.update({id}, {
$inc: {__v: 1},
$set: {
password: hashedPassword
}
});
}
/**
@@ -301,54 +274,48 @@ module.exports = class UsersService {
* @param {String} username name of the display user
* @param {Function} done callback
*/
static createLocalUser(email, password, username) {
static async createLocalUser(email, password, username) {
if (!email) {
return Promise.reject(errors.ErrMissingEmail);
throw errors.ErrMissingEmail;
}
email = email.toLowerCase().trim();
username = username.trim();
return Promise.all([
await Promise.all([
UsersService.isValidUsername(username),
UsersService.isValidPassword(password)
])
.then(() => { // username is valid
return new Promise((resolve, reject) => {
bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => {
if (err) {
return reject(err);
}
]);
let user = new UserModel({
username,
lowercaseUsername: username.toLowerCase(),
password: hashedPassword,
roles: [],
profiles: [
{
id: email,
provider: 'local'
}
]
});
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
user.save((err) => {
if (err) {
if (err.code === 11000) {
if (err.message.match('Username')) {
return reject(errors.ErrUsernameTaken);
}
return reject(errors.ErrEmailTaken);
}
return reject(err);
}
return resolve(user);
});
});
});
});
let user = new UserModel({
username,
lowercaseUsername: username.toLowerCase(),
password: hashedPassword,
roles: [],
profiles: [
{
id: email,
provider: 'local'
}
]
});
try {
user = await user.save();
} catch (err) {
if (err.code === 11000) {
if (err.message.match('Username')) {
throw errors.ErrUsernameTaken;
}
throw errors.ErrEmailTaken;
}
throw err;
}
return user;
}
/**
@@ -387,14 +354,14 @@ module.exports = class UsersService {
* @param {String} role role to add
* @param {Function} done callback after the operation is complete
*/
static addRoleToUser(id, role) {
static async addRoleToUser(id, role) {
const roles = [];
// Check to see if the user role is in the allowable set of roles.
if (role && USER_ROLES.indexOf(role) === -1) {
// User role is not supported! Error out here.
return Promise.reject(new Error(`role ${role} is not supported`));
throw new Error(`role ${role} is not supported`);
} else if(role) {
roles.push(role);
}
@@ -408,13 +375,13 @@ module.exports = class UsersService {
* @param {String} role role to remove
* @param {Function} done callback after the operation is complete
*/
static removeRoleFromUser(id, role) {
static async removeRoleFromUser(id, role) {
// Check to see if the user role is in the allowable set of roles.
if (USER_ROLES.indexOf(role) === -1) {
// User role is not supported! Error out here.
return Promise.reject(new Error(`role ${role} is not supported`));
throw new Error(`role ${role} is not supported`);
}
return UserModel.update({id}, {
@@ -430,13 +397,13 @@ module.exports = class UsersService {
* @param {String} status status to set
* @param {Function} done callback after the operation is complete
*/
static setStatus(id, status) {
static async setStatus(id, status) {
// Check to see if the user status is in the allowable set of roles.
if (USER_STATUS.indexOf(status) === -1) {
// User status is not supported! Error out here.
return Promise.reject(new Error(`status ${status} is not supported`));
throw new Error(`status ${status} is not supported`);
}
// TODO: current updating status behavior is weird.
@@ -583,53 +550,52 @@ module.exports = class UsersService {
* Creates a JWT from a user email. Only works for local accounts.
* @param {String} email of the local user
*/
static createPasswordResetToken(email, loc) {
static async createPasswordResetToken(email, loc) {
if (!email || typeof email !== 'string') {
return Promise.reject('email is required when creating a JWT for resetting passord');
throw new Error('email is required when creating a JWT for resetting passord');
}
email = email.toLowerCase();
return Promise.all([
const [user, settings] = await Promise.all([
UserModel.findOne({profiles: {$elemMatch: {id: email}}}),
SettingsService.retrieve()
])
.then(([user, settings]) => {
if (!user) {
SettingsService.retrieve(),
]);
// Since we don't want to reveal that the email does/doesn't exist
// just go ahead and resolve the Promise with null and check in the
// endpoint.
return;
}
let redirectDomain;
try {
const {hostname, port} = url.parse(loc);
redirectDomain = hostname;
if (port) {
redirectDomain += `:${port}`;
}
} catch (e) {
return Promise.reject('redirect location is invalid');
}
if (!user) {
if (settings.domains.whitelist.indexOf(redirectDomain) === -1) {
return Promise.reject('redirect location is not on the list of acceptable domains');
}
// Since we don't want to reveal that the email does/doesn't exist
// just go ahead and resolve the Promise with null and check in the
// endpoint.
return;
}
let redirectDomain;
try {
const {hostname, port} = url.parse(loc);
redirectDomain = hostname;
if (port) {
redirectDomain += `:${port}`;
}
} catch (e) {
throw new Error('redirect location is invalid');
}
const payload = {
jti: uuid.v4(),
email,
loc,
userId: user.id,
version: user.__v
};
if (settings.domains.whitelist.indexOf(redirectDomain) === -1) {
throw new Error('redirect location is not on the list of acceptable domains');
}
return JWT_SECRET.sign(payload, {
expiresIn: '1d',
subject: PASSWORD_RESET_JWT_SUBJECT
});
});
const payload = {
jti: uuid.v4(),
email,
loc,
userId: user.id,
version: user.__v
};
return JWT_SECRET.sign(payload, {
expiresIn: '1d',
subject: PASSWORD_RESET_JWT_SUBJECT
});
}
/**
@@ -755,7 +721,7 @@ module.exports = class UsersService {
*/
static async createEmailConfirmToken(userID = null, email, referer = ROOT_URL) {
if (!email || typeof email !== 'string') {
return Promise.reject('email is required when creating a JWT for resetting passord');
throw new Error('email is required when creating a JWT for resetting passord');
}
// Conform the email to lowercase.
+1 -7
View File
@@ -3,12 +3,6 @@ const cache = require('../../services/cache');
const client = createClient();
beforeEach(() => Promise.all([
new Promise((resolve, reject) => client.flushdb((err) => {
if (err) {
return reject(err);
}
return resolve();
})),
client.flushdb(),
cache.init(),
]));
+128 -233
View File
@@ -63,20 +63,13 @@ abbrev@1, abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
accepts@^1.3.4:
accepts@^1.3.4, accepts@~1.3.3:
version "1.3.4"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f"
dependencies:
mime-types "~2.1.16"
negotiator "0.6.1"
accepts@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
dependencies:
mime-types "~2.1.11"
negotiator "0.6.1"
acorn-dynamic-import@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4"
@@ -356,18 +349,12 @@ async@2.1.4:
dependencies:
lodash "^4.14.0"
async@2.4.1:
async@2.4.1, async@^2.1.2, async@^2.1.4:
version "2.4.1"
resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
dependencies:
lodash "^4.14.0"
async@^2.0.1, async@^2.1.2, async@^2.1.4:
version "2.5.0"
resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
dependencies:
lodash "^4.14.0"
async@~0.9.0:
version "0.9.2"
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
@@ -416,15 +403,7 @@ babel-cli@^6.26.0:
optionalDependencies:
chokidar "^1.6.1"
babel-code-frame@^6.11.0, babel-code-frame@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
dependencies:
chalk "^1.1.0"
esutils "^2.0.2"
js-tokens "^3.0.0"
babel-code-frame@^6.26.0:
babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
dependencies:
@@ -465,20 +444,7 @@ babel-eslint@^7.2.1:
babel-types "^6.23.0"
babylon "^6.17.0"
babel-generator@^6.18.0:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.1.tgz#e715f486c58ded25649d888944d52aa07c5d9497"
dependencies:
babel-messages "^6.23.0"
babel-runtime "^6.22.0"
babel-types "^6.24.1"
detect-indent "^4.0.0"
jsesc "^1.3.0"
lodash "^4.2.0"
source-map "^0.5.0"
trim-right "^1.0.1"
babel-generator@^6.26.0:
babel-generator@^6.18.0, babel-generator@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5"
dependencies:
@@ -956,31 +922,14 @@ babel-register@^6.26.0:
mkdirp "^0.5.1"
source-map-support "^0.4.15"
babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.6.1:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.10.0"
babel-runtime@^6.26.0:
babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.6.1:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.3.0:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.24.1.tgz#04ae514f1f93b3a2537f2a0f60a5a45fb8308333"
dependencies:
babel-runtime "^6.22.0"
babel-traverse "^6.24.1"
babel-types "^6.24.1"
babylon "^6.11.0"
lodash "^4.2.0"
babel-template@^6.26.0:
babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
dependencies:
@@ -990,21 +939,7 @@ babel-template@^6.26.0:
babylon "^6.18.0"
lodash "^4.17.4"
babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.24.1.tgz#ab36673fd356f9a0948659e7b338d5feadb31695"
dependencies:
babel-code-frame "^6.22.0"
babel-messages "^6.23.0"
babel-runtime "^6.22.0"
babel-types "^6.24.1"
babylon "^6.15.0"
debug "^2.2.0"
globals "^9.0.0"
invariant "^2.2.0"
lodash "^4.2.0"
babel-traverse@^6.26.0:
babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
dependencies:
@@ -1018,16 +953,7 @@ babel-traverse@^6.26.0:
invariant "^2.2.2"
lodash "^4.17.4"
babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.24.1.tgz#a136879dc15b3606bda0d90c1fc74304c2ff0975"
dependencies:
babel-runtime "^6.22.0"
esutils "^2.0.2"
lodash "^4.2.0"
to-fast-properties "^1.0.1"
babel-types@^6.26.0:
babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
dependencies:
@@ -1036,11 +962,7 @@ babel-types@^6.26.0:
lodash "^4.17.4"
to-fast-properties "^1.0.3"
babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0, babylon@^6.17.0:
version "6.17.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.0.tgz#37da948878488b9c4e3c4038893fa3314b3fc932"
babylon@^6.18.0:
babylon@^6.13.0, babylon@^6.17.0, babylon@^6.18.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
@@ -1056,7 +978,7 @@ balanced-match@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.2.1.tgz#7bc658b4bed61eee424ad74f75f5c3e2c4df3cc7"
balanced-match@^0.4.1, balanced-match@^0.4.2:
balanced-match@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
@@ -1112,7 +1034,7 @@ bluebird@2.9.24:
version "2.9.24"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.9.24.tgz#14a2e75f0548323dc35aa440d92007ca154e967c"
bluebird@3.5.0:
bluebird@3.5.0, bluebird@^3.3.4, bluebird@^3.4.6:
version "3.5.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
@@ -1120,30 +1042,11 @@ bluebird@^2.10.2:
version "2.11.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
bluebird@^3.4.6:
version "3.4.7"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.6"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215"
body-parser@^1.12.2:
version "1.17.1"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.1.tgz#75b3bc98ddd6e7e0d8ffe750dfaca5c66993fa47"
dependencies:
bytes "2.4.0"
content-type "~1.0.2"
debug "2.6.1"
depd "~1.1.0"
http-errors "~1.6.1"
iconv-lite "0.4.15"
on-finished "~2.3.0"
qs "6.4.0"
raw-body "~2.2.0"
type-is "~1.6.14"
body-parser@^1.17.2:
body-parser@^1.12.2, body-parser@^1.17.2:
version "1.17.2"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee"
dependencies:
@@ -1172,13 +1075,6 @@ bowser@^1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.7.2.tgz#b94cc6925ba6b5e07c421a58e601ce4611264572"
brace-expansion@^1.0.0:
version "1.1.7"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59"
dependencies:
balanced-match "^0.4.1"
concat-map "0.0.1"
brace-expansion@^1.1.7:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
@@ -1394,7 +1290,7 @@ chalk@2.0.1:
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
dependencies:
@@ -1602,6 +1498,10 @@ clone@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
cluster-key-slot@^1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.0.8.tgz#7654556085a65330932a2e8b5976f8e2d0b3e414"
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -1863,11 +1763,7 @@ core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
core-js@^2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
core-js@^2.5.0:
core-js@^2.4.0, core-js@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086"
@@ -2127,9 +2023,9 @@ date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
debug@*:
version "3.0.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.0.tgz#1d2feae53349047b08b264ec41906ba17a8516e4"
debug@*, debug@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.1.tgz#0564c612b521dc92d9f2988f0549e34f9c98db64"
dependencies:
ms "2.0.0"
@@ -2139,12 +2035,6 @@ debug@2.6.0:
dependencies:
ms "0.7.2"
debug@2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351"
dependencies:
ms "0.7.2"
debug@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e"
@@ -2157,12 +2047,6 @@ debug@2.6.8, debug@^2.2.0, debug@^2.6.8:
dependencies:
ms "2.0.0"
debug@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.0.1.tgz#0564c612b521dc92d9f2988f0549e34f9c98db64"
dependencies:
ms "2.0.0"
debug@~0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39"
@@ -2236,6 +2120,10 @@ delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
denque@^1.1.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/denque/-/denque-1.2.2.tgz#e06cf7cf0da8badc88cbdaabf8fc0a70d659f1d4"
depd@1.1.0, depd@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3"
@@ -2491,7 +2379,7 @@ es6-promise@3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.2.1.tgz#ec56233868032909207170c39448e24449dd1fc4"
es6-promise@^3.0.2, es6-promise@^3.2.1:
es6-promise@^3.0.2:
version "3.3.1"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
@@ -2915,6 +2803,10 @@ flatten@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
flexbuffer@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/flexbuffer/-/flexbuffer-0.0.6.tgz#039fdf23f8823e440c38f3277e6fef1174215b30"
for-in@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -3188,7 +3080,7 @@ glob@7.0.x:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
glob@7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
dependencies:
@@ -3199,7 +3091,7 @@ glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@7.1.2, glob@^7.1.2:
glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -3230,10 +3122,6 @@ glob@^6.0.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
globals@^9.0.0:
version "9.17.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.17.0.tgz#0c0ca696d9b9bb694d2e5470bd37777caad50286"
globals@^9.17.0, globals@^9.18.0:
version "9.18.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
@@ -3331,13 +3219,14 @@ graphql-errors@^2.1.0:
babel-runtime "^6.6.1"
uuid "^2.0.2"
graphql-redis-subscriptions@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/graphql-redis-subscriptions/-/graphql-redis-subscriptions-1.1.5.tgz#69166bebd8d92243d6f8e80c97ebf4c588d80c8d"
graphql-redis-subscriptions@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/graphql-redis-subscriptions/-/graphql-redis-subscriptions-1.3.0.tgz#bbc52b0f77bf7d50945c6bf4e8b8aba5135555b4"
dependencies:
async "^2.0.1"
graphql-subscriptions "^0.2.0"
redis "^2.6.3"
graphql-subscriptions "^0.4.2"
iterall "^1.1.1"
optionalDependencies:
ioredis "^3.1.2"
graphql-server-core@^0.6.0:
version "0.6.0"
@@ -3359,13 +3248,7 @@ graphql-server-module-graphiql@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/graphql-server-module-graphiql/-/graphql-server-module-graphiql-0.6.0.tgz#e37634b05f000731981e8ed13103f9a5861e5da0"
graphql-subscriptions@^0.2.0:
version "0.2.3"
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.2.3.tgz#75f9a02cb6d2b456b1703ce95670f793dacee607"
dependencies:
es6-promise "^3.2.1"
graphql-subscriptions@^0.4.3:
graphql-subscriptions@^0.4.2, graphql-subscriptions@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.4.3.tgz#2aed6ba87551cc747742b793497ff24b22991867"
dependencies:
@@ -3817,6 +3700,34 @@ invert-kv@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
ioredis@^3.1.2, ioredis@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-3.1.4.tgz#8688293f5f2f1757e1c812ad17cce49f46d811bc"
dependencies:
bluebird "^3.3.4"
cluster-key-slot "^1.0.6"
debug "^2.2.0"
denque "^1.1.0"
flexbuffer "0.0.6"
lodash.assign "^4.2.0"
lodash.bind "^4.2.1"
lodash.clone "^4.5.0"
lodash.clonedeep "^4.5.0"
lodash.defaults "^4.2.0"
lodash.difference "^4.5.0"
lodash.flatten "^4.4.0"
lodash.foreach "^4.5.0"
lodash.isempty "^4.4.0"
lodash.keys "^4.2.0"
lodash.noop "^3.0.1"
lodash.partial "^4.2.1"
lodash.pick "^4.4.0"
lodash.sample "^4.2.1"
lodash.shuffle "^4.2.0"
lodash.values "^4.3.0"
redis-commands "^1.2.0"
redis-parser "^2.4.0"
ip-regex@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd"
@@ -4197,22 +4108,18 @@ js-stringify@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db"
js-tokens@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
js-tokens@^3.0.2:
js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.2, js-yaml@^3.7.0:
js-yaml@3.x, js-yaml@^3.5.2:
version "3.8.3"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.3.tgz#33a05ec481c850c8875929166fe1beb61c728766"
dependencies:
argparse "^1.0.7"
esprima "^3.1.1"
js-yaml@^3.9.1:
js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.9.1:
version "3.9.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0"
dependencies:
@@ -4275,11 +4182,7 @@ jshint@^2.8.0:
shelljs "0.3.x"
strip-json-comments "1.0.x"
json-loader@^0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de"
json-loader@^0.5.7:
json-loader@^0.5.4, json-loader@^0.5.7:
version "0.5.7"
resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
@@ -4597,7 +4500,7 @@ lodash.assignin@^4.0.9:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2"
lodash.bind@^4.1.4:
lodash.bind@^4.1.4, lodash.bind@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35"
@@ -4605,6 +4508,14 @@ lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
lodash.clone@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6"
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
lodash.create@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7"
@@ -4620,19 +4531,23 @@ lodash.defaults@^3.1.2:
lodash.assign "^3.0.0"
lodash.restparam "^3.0.0"
lodash.defaults@^4.0.1:
lodash.defaults@^4.0.1, lodash.defaults@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
lodash.difference@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c"
lodash.filter@^4.4.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
lodash.flatten@^4.2.0:
lodash.flatten@^4.2.0, lodash.flatten@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
lodash.foreach@^4.3.0:
lodash.foreach@^4.3.0, lodash.foreach@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
@@ -4644,6 +4559,10 @@ lodash.isarray@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
lodash.isempty@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
lodash.isequal@^4.1.1, lodash.isequal@^4.4.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
@@ -4664,6 +4583,10 @@ lodash.keys@^3.0.0:
lodash.isarguments "^3.0.0"
lodash.isarray "^3.0.0"
lodash.keys@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-4.2.0.tgz#a08602ac12e4fb83f91fc1fb7a360a4d9ba35205"
lodash.map@^4.4.0, lodash.map@^4.5.1:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
@@ -4676,10 +4599,18 @@ lodash.merge@^4.4.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
lodash.noop@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c"
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
lodash.partial@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/lodash.partial/-/lodash.partial-4.2.1.tgz#49f3d8cfdaa3bff8b3a91d127e923245418961d4"
lodash.pick@^4.2.1, lodash.pick@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
@@ -4696,6 +4627,14 @@ lodash.restparam@^3.0.0:
version "3.6.1"
resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
lodash.sample@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/lodash.sample/-/lodash.sample-4.2.1.tgz#5e4291b0c753fa1abeb0aab8fb29df1b66f07f6d"
lodash.shuffle@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.shuffle/-/lodash.shuffle-4.2.0.tgz#145b5053cf875f6f5c2a33f48b6e9948c6ec7b4b"
lodash.some@^4.4.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
@@ -4708,6 +4647,10 @@ lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
lodash.values@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347"
lodash@3.10.1, lodash@^3.3.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
@@ -4871,15 +4814,11 @@ mime-db@~1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.12.0.tgz#3d0c63180f458eb10d325aaa37d7c58ae312e9d7"
mime-db@~1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
mime-types@^2.1.10, mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7:
version "2.1.15"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
mime-types@^2.1.10, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7:
version "2.1.16"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23"
dependencies:
mime-db "~1.27.0"
mime-db "~1.29.0"
mime-types@~2.0.3:
version "2.0.14"
@@ -4887,12 +4826,6 @@ mime-types@~2.0.3:
dependencies:
mime-db "~1.12.0"
mime-types@~2.1.16:
version "2.1.16"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23"
dependencies:
mime-db "~1.29.0"
mime@1.3.4, mime@1.3.x, mime@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
@@ -4909,13 +4842,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
dependencies:
brace-expansion "^1.0.0"
minimatch@^3.0.4, minimatch@~3.0.2:
"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies:
@@ -4960,7 +4887,7 @@ mocha@^3.1.2:
mkdirp "0.5.1"
supports-color "3.1.2"
moment@2.18.1, moment@2.x.x, moment@^2.10.3:
moment@2.18.1, moment@2.x.x, moment@^2.10.3, moment@^2.18.1:
version "2.18.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
@@ -6346,7 +6273,7 @@ pym.js@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pym.js/-/pym.js-1.2.0.tgz#feb1e2c9b396613e5172192b0cdd75408f9c8322"
q@1.5.0:
q@1.5.0, q@^1.1.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
@@ -6358,10 +6285,6 @@ q@2.0.3:
pop-iterate "^1.0.1"
weak-map "^1.0.5"
q@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/q/-/q-1.1.2.tgz#6357e291206701d99f197ab84e57e8ad196f2a89"
qs@6.4.0, qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
@@ -6655,7 +6578,7 @@ redis-commands@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.1.tgz#81d826f45fa9c8b2011f4cd7a0fe597d241d442b"
redis-parser@^2.0.0, redis-parser@^2.6.0:
redis-parser@^2.0.0, redis-parser@^2.4.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b"
@@ -6663,14 +6586,6 @@ redis@^0.12.1:
version "0.12.1"
resolved "https://registry.yarnpkg.com/redis/-/redis-0.12.1.tgz#64df76ad0fc8acebaebd2a0645e8a48fac49185e"
redis@^2.6.3, redis@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02"
dependencies:
double-ended-queue "^2.1.0-0"
redis-commands "^1.2.0"
redis-parser "^2.6.0"
redis@~2.6.0-2:
version "2.6.5"
resolved "https://registry.yarnpkg.com/redis/-/redis-2.6.5.tgz#87c1eff4a489f94b70871f3d08b6988f23a95687"
@@ -6721,10 +6636,6 @@ regenerate@^1.2.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260"
regenerator-runtime@^0.10.0:
version "0.10.4"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.4.tgz#74cb6598d3ba2eb18694e968a40e2b3b4df9cf93"
regenerator-runtime@^0.10.5:
version "0.10.5"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658"
@@ -7052,7 +6963,7 @@ shelljs@0.3.x:
version "0.3.0"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1"
shelljs@0.7.8:
shelljs@0.7.8, shelljs@^0.7.0:
version "0.7.8"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3"
dependencies:
@@ -7060,14 +6971,6 @@ shelljs@0.7.8:
interpret "^1.0.0"
rechoir "^0.6.2"
shelljs@^0.7.0:
version "0.7.7"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1"
dependencies:
glob "^7.0.0"
interpret "^1.0.0"
rechoir "^0.6.2"
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@@ -7200,7 +7103,7 @@ source-map@0.4.x, source-map@^0.4.4:
dependencies:
amdefine ">=0.0.4"
source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3:
source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
@@ -7573,10 +7476,6 @@ to-capital-case@^1.0.0:
dependencies:
to-space-case "^1.0.0"
to-fast-properties@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320"
to-fast-properties@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
@@ -7678,7 +7577,7 @@ type-detect@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea"
type-is@~1.6.14, type-is@~1.6.15:
type-is@~1.6.15:
version "1.6.15"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
dependencies:
@@ -7976,14 +7875,10 @@ word-wrap@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.1.0.tgz#356153d61d10610d600785c5d701288e0ae764a6"
word-wrap@1.2.3:
word-wrap@1.2.3, word-wrap@^1.0.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
word-wrap@^1.0.3:
version "1.2.1"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.1.tgz#248f459b465d179a17bc407c854d3151d07e45d8"
wordwrap@0.0.2, wordwrap@~0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"