Merge branch 'master' into docs

This commit is contained in:
Wyatt Johnson
2018-02-22 15:40:06 -07:00
98 changed files with 1324 additions and 969 deletions
+2 -1
View File
@@ -33,4 +33,5 @@ public
!plugins/talk-plugin-sort-oldest
!plugins/talk-plugin-subscriber
!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-viewing-options
!plugins/talk-plugin-viewing-options
!plugins/talk-plugin-profile-settings
+2
View File
@@ -29,6 +29,7 @@ plugins.json
plugins/*
!plugins/talk-plugin-akismet
!plugins/talk-plugin-facebook-auth
!plugins/talk-plugin-google-auth
!plugins/talk-plugin-auth
!plugins/talk-plugin-respect
!plugins/talk-plugin-offtopic
@@ -56,6 +57,7 @@ plugins/*
!plugins/talk-plugin-subscriber
!plugins/talk-plugin-flag-details
!plugins/talk-plugin-slack-notifications
!plugins/talk-plugin-profile-settings
**/node_modules/*
yarn-error.log
+2 -1
View File
@@ -1,6 +1,7 @@
{
"exceptions": [
"https://nodesecurity.io/advisories/531",
"https://nodesecurity.io/advisories/532"
"https://nodesecurity.io/advisories/532",
"https://nodesecurity.io/advisories/566"
]
}
-1
View File
@@ -18,7 +18,6 @@ ENV NODE_ENV production
# Install app dependencies and build static assets.
RUN yarn global add node-gyp && \
yarn install --frozen-lockfile && \
cli plugins reconcile && \
yarn build && \
yarn cache clean
+3
View File
@@ -1,6 +1,9 @@
FROM coralproject/talk:latest
# Setup the build arguments
ONBUILD ARG TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10
ONBUILD ARG TALK_ASSET_COMMENTS_LOAD_DEPTH=10
ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
ONBUILD ARG TALK_THREADING_LEVEL=3
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
ONBUILD ARG TALK_DEFAULT_LANG=en
+29 -97
View File
@@ -135,18 +135,11 @@ function reconcilePackages({ quiet = false, upgradeRemote = false }) {
return { local, fetchable, upgradable };
}
async function reconcileRemotePlugins({ skipLocal, dryRun, upgradeRemote }) {
console.log(
`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get(
'mag'
)} Reconciling plugins...`.yellow
);
async function reconcileRemotePlugins({ dryRun, upgradeRemote }) {
console.log(`\n['1/2'] ${emoji.get('mag')} Reconciling plugins...`.yellow);
const { fetchable, upgradable } = reconcilePackages({ upgradeRemote });
console.log(
`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`
.yellow
);
console.log(`['2/2'] ${emoji.get('truck')} Fetching plugins...\n`.yellow);
if (fetchable.length > 0) {
console.log(
@@ -206,98 +199,41 @@ async function reconcileRemotePlugins({ skipLocal, dryRun, upgradeRemote }) {
return { upgradable, fetchable };
}
async function reconcileLocalPlugins({ skipRemote, dryRun }) {
console.log(
`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get(
'pick'
)} Installing local plugin dependencies...\n`.yellow
);
const { local } = reconcilePackages({ quiet: true });
for (let i in local) {
let { name } = local[i];
if (!fs.existsSync(path.join(dir, 'plugins', name, 'package.json'))) {
continue;
}
let wd = path.join(dir, 'plugins', name);
console.log(`$ cd ${wd.cyan} && yarn`);
if (!dryRun) {
let args = [];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit'],
cwd: wd,
});
if (output.status) {
throw new Error(
'Could not install local plugin dependencies, errors occurred during install'
);
}
console.log(output.stdout.toString());
}
}
}
// This traverses the local plugins and installs any dependencies listed there,
// this only is really needed for plugins that are installed via docker because
// core plugins will have their dependencies already included in core.
async function reconcilePluginDeps({
skipLocal,
skipRemote,
dryRun,
upgradeRemote,
}) {
async function reconcilePluginDeps({ dryRun, upgradeRemote }) {
try {
let startTime = new Date();
// We don't need to do anything if we skip everything....
if (skipLocal && skipRemote) {
return;
}
// Traverse local plugins and install dependencies if enabled.
if (!skipLocal) {
await reconcileLocalPlugins({ skipRemote, dryRun });
}
// Locate any external plugins and install them.
if (!skipRemote) {
const results = await reconcileRemotePlugins({
skipLocal,
skipRemote,
dryRun,
upgradeRemote,
});
const results = await reconcileRemotePlugins({
dryRun,
upgradeRemote,
});
let status;
if (dryRun) {
status = '[dry-run] success'.green;
} else {
status = 'success'.green;
}
let message;
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
message = 'Already up-to-date.';
} else if (results.upgradable.length === 0) {
message = `Fetched ${results.fetchable.length} new plugins.`;
} else if (results.fetchable.length === 0) {
message = `Upgraded ${results.upgradable.length} new plugins.`;
} else {
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
results.upgradable.length
} plugins.`;
}
console.log(`\n${status} ${message}`);
let status;
if (dryRun) {
status = '[dry-run] success'.green;
} else {
status = 'success'.green;
}
let message;
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
message = 'Already up-to-date.';
} else if (results.upgradable.length === 0) {
message = `Fetched ${results.fetchable.length} new plugins.`;
} else if (results.fetchable.length === 0) {
message = `Upgraded ${results.upgradable.length} new plugins.`;
} else {
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
results.upgradable.length
} plugins.`;
}
console.log(`\n${status} ${message}`);
let endTime = new Date();
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(
@@ -440,16 +376,12 @@ program
program
.command('reconcile')
.description(
'reconciles local plugin dependencies and downloads external plugins'
)
.description('reconciles dependencies by downloading external plugins')
.option('-u, --upgrade-remote', 'upgrades remote dependencies')
.option(
'-d, --dry-run',
'does not actually change anything on the filesystem acts only as a simulation'
)
.option('--skip-local', 'skips the local dependancy reconciliation')
.option('--skip-remote', 'skips the remote plugin reconciliation')
.action(reconcilePluginDeps);
program.parse(process.argv);
@@ -19,14 +19,30 @@ const buildUserHistory = (userState = {}) => {
);
};
const buildActionResponse = (typename, until, status) => {
/** readableDuration returns a readable duration of the suspension/ban in hours or days
* @param {} startDate
* @param {} endDate
*/
const readableDuration = (startDate, endDate) => {
const dur = moment.duration(moment(endDate).diff(moment(startDate)));
const durAsDays = dur.asDays().toFixed(0);
const durAsHours = dur.asHours().toFixed(0);
return durAsHours > 23
? `${durAsDays} ${durAsDays > 1 ? 'days' : 'day'}`
: `${durAsHours} ${durAsHours > 1 ? 'hours' : 'hour'}`;
};
const buildActionResponse = (typename, created_at, until, status) => {
switch (typename) {
case 'UsernameStatusHistory':
return `Username ${status}`;
case 'BannedStatusHistory':
return status ? 'User banned' : 'Ban removed';
case 'SuspensionStatusHistory':
return until ? 'Account Suspended' : 'Suspension removed';
return until
? `Suspended, ${readableDuration(created_at, until)}`
: 'Suspension removed';
default:
return '-';
}
@@ -77,7 +93,7 @@ class AccountHistory extends React.Component {
'talk-admin-account-history-row-status'
)}
>
{buildActionResponse(__typename, until, status)}
{buildActionResponse(__typename, created_at, until, status)}
</div>
<div
className={cn(
@@ -75,7 +75,7 @@ ForgotPassword.propTypes = {
email: PropTypes.string.isRequired,
onEmailChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
onSignInLink: PropTypes.func.isRequired,
};
+1 -1
View File
@@ -87,7 +87,7 @@ SignIn.propTypes = {
onForgotPasswordLink: PropTypes.func.isRequired,
onRecaptchaVerify: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
requireRecaptcha: PropTypes.bool.isRequired,
};
@@ -28,8 +28,8 @@
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: 10;
top: 32px;
right: 0px;
width: 140px;
left: -100px;
width: 200px;
text-align: left;
color: #616161;
}
@@ -39,7 +39,7 @@
border: 10px solid transparent;
border-top-color: #999;
position: absolute;
right: 0px;
left: 96px;
top: -20px;
transform: rotate(180deg);
}
@@ -49,7 +49,7 @@
border: 10px solid transparent;
border-top-color: white;
position: absolute;
right: 0px;
left: 96px;
top: -19px;
transform: rotate(180deg);
}
@@ -67,7 +67,7 @@ class UserInfoTooltip extends React.Component {
new Date(
this.getLastHistoryItem(user, 'banned').created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
).format('MMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
@@ -139,7 +139,7 @@ class UserInfoTooltip extends React.Component {
'suspension'
).created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
).format('MMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
@@ -154,7 +154,7 @@ class UserInfoTooltip extends React.Component {
new Date(
this.getLastHistoryItem(user, 'suspension').until
)
).format('MMMM Do YYYY, h:mm:ss a')}
).format('MMM Do YYYY, h:mm:ss a')}
</span>
</li>
</ul>
@@ -34,7 +34,7 @@ class ForgotPasswordContainer extends Component {
ForgotPasswordContainer.propTypes = {
success: PropTypes.bool.isRequired,
forgotPassword: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
onSignInLink: PropTypes.func.isRequired,
};
+1 -1
View File
@@ -50,7 +50,7 @@ class SignInContainer extends Component {
SignInContainer.propTypes = {
signIn: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
onForgotPasswordLink: PropTypes.func.isRequired,
requireRecaptcha: PropTypes.bool.isRequired,
};
@@ -143,7 +143,6 @@
i {
font-size: 12px;
top: 2px;
position: relative;
}
}
@@ -0,0 +1,3 @@
import * as actions from '../constants/profile';
export const setActiveTab = tab => ({ type: actions.SET_ACTIVE_TAB, tab });
@@ -9,16 +9,12 @@ import AutomaticAssetClosure from '../containers/AutomaticAssetClosure';
import ExtendableTabPanel from '../containers/ExtendableTabPanel';
import { Tab, TabPane } from 'coral-ui';
import ProfileContainer from '../tabs/profile/containers/ProfileContainer';
import Profile from '../tabs/profile/containers/Profile';
import Popup from 'coral-framework/components/Popup';
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
import cn from 'classnames';
export default class Embed extends React.Component {
changeTab = tab => {
this.props.setActiveTab(tab);
};
getTabs() {
const tabs = [
<Tab
@@ -53,6 +49,7 @@ export default class Embed extends React.Component {
render() {
const {
activeTab,
setActiveTab,
commentId,
root,
root: { asset },
@@ -65,6 +62,7 @@ export default class Embed extends React.Component {
parentUrl,
} = this.props;
const hasHighlightedComment = !!commentId;
const popupUrl = `login?parentUrl=${encodeURIComponent(parentUrl)}`;
return (
<div
@@ -75,7 +73,7 @@ export default class Embed extends React.Component {
<AutomaticAssetClosure asset={asset} />
<IfSlotIsNotEmpty slot="login">
<Popup
href={`login?parentUrl=${encodeURIComponent(parentUrl)}`}
href={popupUrl}
title="Login"
features="menubar=0,resizable=0,width=500,height=550,top=200,left=500"
open={showSignInDialog}
@@ -91,7 +89,7 @@ export default class Embed extends React.Component {
<ExtendableTabPanel
className="talk-embed-stream-tab-bar"
activeTab={activeTab}
setActiveTab={this.changeTab}
setActiveTab={setActiveTab}
fallbackTab="stream"
tabSlot="embedStreamTabs"
tabSlotPrepend="embedStreamTabsPrepend"
@@ -112,7 +110,7 @@ export default class Embed extends React.Component {
tabId="profile"
className="talk-embed-stream-profile-tab-pane"
>
<ProfileContainer />
<Profile />
</TabPane>,
<TabPane
key="config"
@@ -0,0 +1,2 @@
const prefix = 'TALK_EMBED_STREAM';
export const SET_ACTIVE_TAB = `${prefix}_SET_ACTIVE_TAB`;
@@ -1,14 +1,27 @@
import defaultTo from 'lodash/defaultTo';
const prefix = 'TALK_EMBED_STREAM';
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
export const VIEW_COMMENT = 'VIEW_COMMENT';
export const ADDTL_COMMENTS_ON_LOAD_MORE = parseInt(
defaultTo(process.env.TALK_ADDTL_COMMENTS_ON_LOAD_MORE, '10')
);
export const ASSET_COMMENTS_LOAD_DEPTH = parseInt(
defaultTo(process.env.TALK_ASSET_COMMENTS_LOAD_DEPTH, '10')
);
export const REPLY_COMMENTS_LOAD_DEPTH = parseInt(
defaultTo(process.env.TALK_REPLY_COMMENTS_LOAD_DEPTH, '3')
);
export const THREADING_LEVEL = parseInt(
defaultTo(process.env.TALK_THREADING_LEVEL, '3')
);
export const ADD_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_ADD_TAG`;
export const ADD_COMMENT_CLASSNAME = 'ADD_COMMENT_CLASSNAME';
export const CLEAR_COMMENT_BOX_TAGS = `${prefix}_COMMENT_BOX_CLEAR_TAGS`;
export const REMOVE_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_REMOVE_TAG`;
export const REMOVE_COMMENT_CLASSNAME = 'REMOVE_COMMENT_CLASSNAME';
export const THREADING_LEVEL = process.env.TALK_THREADING_LEVEL;
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
export const SET_ACTIVE_TAB = 'CORAL_STREAM_SET_ACTIVE_TAB';
export const SET_SORT = 'CORAL_STREAM_SET_SORT';
export const ADD_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_ADD_TAG`;
export const REMOVE_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_REMOVE_TAG`;
export const CLEAR_COMMENT_BOX_TAGS = `${prefix}_COMMENT_BOX_CLEAR_TAGS`;
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
export const VIEW_COMMENT = 'VIEW_COMMENT';
@@ -3,6 +3,7 @@ import asset from './asset';
import embed from './embed';
import configure from './configure';
import stream from './stream';
import profile from './profile';
export default {
login,
@@ -10,4 +11,5 @@ export default {
embed,
configure,
stream,
profile,
};
@@ -0,0 +1,19 @@
import * as actions from '../constants/profile';
const initialState = {
activeTab: 'comments',
previousTab: '',
};
export default function stream(state = initialState, action) {
switch (action.type) {
case actions.SET_ACTIVE_TAB:
return {
...state,
activeTab: action.tab,
previousTab: state.activeTab,
};
default:
return state;
}
}
@@ -11,8 +11,18 @@ import { getTotalReactionsCount } from 'coral-framework/utils';
import t from 'coral-framework/services/i18n';
class Comment extends React.Component {
goToStory = () => {
this.props.navigate(this.props.comment.asset.url);
};
goToConversation = () => {
this.props.navigate(
`${this.props.comment.asset.url}?commentId=${this.props.comment.id}`
);
};
render() {
const { comment, link, data, root } = this.props;
const { comment, data, root } = this.props;
const reactionCount = getTotalReactionsCount(comment.action_summaries);
const queryData = { root, comment, asset: comment.asset };
@@ -67,7 +77,7 @@ class Comment extends React.Component {
<a
className={cn(styles.assetURL, 'my-comment-anchor')}
href="#"
onClick={link(`${comment.asset.url}`)}
onClick={this.goToStory}
>
{t('common.story')}:{' '}
{comment.asset.title ? comment.asset.title : comment.asset.url}
@@ -77,10 +87,7 @@ class Comment extends React.Component {
<div className={styles.sidebar}>
<ul>
<li>
<a
onClick={link(`${comment.asset.url}?commentId=${comment.id}`)}
className={styles.viewLink}
>
<a onClick={this.goToConversation} className={styles.viewLink}>
<Icon name="open_in_new" className={styles.iconView} />
{t('view_conversation')}
</a>
@@ -105,10 +112,10 @@ class Comment extends React.Component {
}
Comment.propTypes = {
comment: PropTypes.shape({
id: PropTypes.string,
body: PropTypes.string,
}).isRequired,
comment: PropTypes.object.isRequired,
navigate: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
};
export default Comment;
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Comment from './Comment';
import Comment from '../containers/Comment';
import LoadMore from './LoadMore';
class CommentHistory extends React.Component {
@@ -21,9 +21,9 @@ class CommentHistory extends React.Component {
};
render() {
const { link, comments, data, root } = this.props;
const { navigate, comments, data, root } = this.props;
return (
<div>
<div className="talk-my-profile-comment-history">
<div className="commentHistory__list">
{comments.nodes.map((comment, i) => {
return (
@@ -32,7 +32,7 @@ class CommentHistory extends React.Component {
data={data}
root={root}
comment={comment}
link={link}
navigate={navigate}
/>
);
})}
@@ -51,7 +51,7 @@ class CommentHistory extends React.Component {
CommentHistory.propTypes = {
comments: PropTypes.object.isRequired,
loadMore: PropTypes.func,
link: PropTypes.func,
navigate: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
};
@@ -0,0 +1,11 @@
.userInfo {
margin-bottom: 20px;
}
.email {
margin: 0;
}
.username {
margin-bottom: 4px;
}
@@ -0,0 +1,57 @@
import React from 'react';
import PropTypes from 'prop-types';
import Slot from 'coral-framework/components/Slot';
import CommentHistory from '../containers/CommentHistory';
import ExtendableTabPanel from '../../../containers/ExtendableTabPanel';
import { Tab, TabPane } from 'coral-ui';
import styles from './Profile.css';
import t from 'coral-framework/services/i18n';
const Profile = ({
username,
emailAddress,
data,
root,
activeTab,
setActiveTab,
}) => (
<div className="talk-my-profile talk-profile-container">
<div className={styles.userInfo}>
<h2 className={styles.username}>{username}</h2>
{emailAddress ? <p className={styles.email}>{emailAddress}</p> : null}
</div>
<Slot fill="profileSections" data={data} queryData={{ root }} />
<ExtendableTabPanel
activeTab={activeTab}
setActiveTab={setActiveTab}
fallbackTab="comments"
tabSlot="profileTabs"
tabSlotPrepend="profileTabsPrepend"
tabPaneSlot="profileTabPanes"
slotProps={{ data }}
queryData={{ root }}
tabs={[
<Tab key="comments" tabId="comments">
{t('framework.my_comments')}
</Tab>,
]}
tabPanes={[
<TabPane key="comments" tabId="comments">
<CommentHistory data={data} root={root} />
</TabPane>,
]}
sub
/>
</div>
);
Profile.propTypes = {
username: PropTypes.string,
emailAddress: PropTypes.string,
data: PropTypes.object,
root: PropTypes.object,
activeTab: PropTypes.string.isRequired,
setActiveTab: PropTypes.func.isRequired,
};
export default Profile;
@@ -0,0 +1,32 @@
import { gql, compose } from 'react-apollo';
import Comment from '../components/Comment';
import { withFragments } from 'coral-framework/hocs';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
const slots = ['commentContent', 'historyCommentTimestamp'];
const withCommentFragments = withFragments({
comment: gql`
fragment TalkEmbedStream_ProfileComment_comment on Comment {
id
body
replyCount
action_summaries {
count
__typename
}
asset {
id
title
url
${getSlotFragmentSpreads(slots, 'asset')}
}
created_at
${getSlotFragmentSpreads(slots, 'comment')}
}
`,
});
const enhance = compose(withCommentFragments);
export default enhance(Comment);
@@ -0,0 +1,103 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose, gql } from 'react-apollo';
import CommentHistory from '../components/CommentHistory';
import Comment from './Comment';
import { withFragments } from 'coral-framework/hocs';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import { getDefinitionName } from 'coral-framework/utils';
class CommentHistoryContainer extends Component {
navigate = url => {
this.context.pym.sendMessage('navigate', url);
};
loadMore = () => {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
limit: 5,
cursor: this.props.root.me.comments.endCursor,
},
updateQuery: (previous, { fetchMoreResult: { me: { comments } } }) => {
const updated = update(previous, {
me: {
comments: {
nodes: {
$apply: nodes => appendNewNodes(nodes, comments.nodes),
},
hasNextPage: { $set: comments.hasNextPage },
endCursor: { $set: comments.endCursor },
},
},
});
return updated;
},
});
};
render() {
return (
<CommentHistory
comments={this.props.root.me.comments}
data={this.props.data}
root={this.props.root}
loadMore={this.loadMore}
navigate={this.navigate}
/>
);
}
}
CommentHistoryContainer.contextTypes = {
pym: PropTypes.object,
};
CommentHistoryContainer.propTypes = {
data: PropTypes.object,
root: PropTypes.object,
};
const LOAD_MORE_QUERY = gql`
query TalkEmbedStream_CommentHistory_LoadMoreComments($limit: Int, $cursor: Cursor) {
me {
comments(query: { limit: $limit, cursor: $cursor }) {
nodes {
...${getDefinitionName(Comment.fragments.comment)}
}
endCursor
hasNextPage
}
}
}
${Comment.fragments.comment}
`;
const withCommentHistoryFragments = withFragments({
root: gql`
fragment TalkEmbedStream_CommentHistory on RootQuery {
me {
comments(query: {limit: 10}) {
nodes {
...${getDefinitionName(Comment.fragments.comment)}
}
endCursor
hasNextPage
}
}
}
${Comment.fragments.comment}
`,
});
const mapStateToProps = state => ({
currentUser: state.auth.user,
});
export default compose(
connect(mapStateToProps, null),
withCommentHistoryFragments
)(CommentHistoryContainer);
@@ -0,0 +1,104 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose, gql } from 'react-apollo';
import { bindActionCreators } from 'redux';
import { withQuery } from 'coral-framework/hocs';
import NotLoggedIn from '../components/NotLoggedIn';
import { Spinner } from 'coral-ui';
import Profile from '../components/Profile';
import CommentHistory from './CommentHistory';
import { getDefinitionName } from 'coral-framework/utils';
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
import { setActiveTab } from '../../../actions/profile';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
class ProfileContainer extends Component {
componentWillReceiveProps(nextProps) {
if (!this.props.currentUser && nextProps.currentUser) {
// Refetch after login.
this.props.data.refetch();
}
}
render() {
const { currentUser, showSignInDialog, root, data } = this.props;
const { me } = this.props.root;
const loading = this.props.data.loading;
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (!currentUser) {
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
}
if (loading || !me) {
return <Spinner />;
}
const localProfile = currentUser.profiles.find(p => p.provider === 'local');
const emailAddress = localProfile && localProfile.id;
return (
<Profile
username={me.username}
emailAddress={emailAddress}
data={data}
root={root}
activeTab={this.props.activeTab}
setActiveTab={this.props.setActiveTab}
/>
);
}
}
ProfileContainer.propTypes = {
data: PropTypes.object,
root: PropTypes.object,
currentUser: PropTypes.object,
showSignInDialog: PropTypes.func,
activeTab: PropTypes.string.isRequired,
setActiveTab: PropTypes.func.isRequired,
};
const slots = [
'profileSections',
'profileTabs',
'profileTabsPrepend',
'profileTabPanes',
];
const withProfileQuery = withQuery(
gql`
query CoralEmbedStream_Profile {
me {
id
username
}
...${getDefinitionName(CommentHistory.fragments.root)}
${getSlotFragmentSpreads(slots, 'root')}
}
${CommentHistory.fragments.root}
`,
{
options: {
fetchPolicy: 'network-only',
},
}
);
const mapStateToProps = state => ({
currentUser: state.auth.user,
activeTab: state.profile.activeTab,
});
const mapDispatchToProps = dispatch =>
bindActionCreators({ showSignInDialog, setActiveTab }, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withProfileQuery
)(ProfileContainer);
@@ -1,178 +0,0 @@
import { connect } from 'react-redux';
import { compose, gql } from 'react-apollo';
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { withQuery } from 'coral-framework/hocs';
import Slot from 'coral-framework/components/Slot';
import cn from 'classnames';
import { link } from 'coral-framework/services/pym';
import NotLoggedIn from '../components/NotLoggedIn';
import { Spinner } from 'coral-ui';
import CommentHistory from '../components/CommentHistory';
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import t from 'coral-framework/services/i18n';
class ProfileContainer extends Component {
componentWillReceiveProps(nextProps) {
if (!this.props.currentUser && nextProps.currentUser) {
// Refetch after login.
this.props.data.refetch();
}
}
loadMore = () => {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
limit: 5,
cursor: this.props.root.me.comments.endCursor,
},
updateQuery: (previous, { fetchMoreResult: { me: { comments } } }) => {
const updated = update(previous, {
me: {
comments: {
nodes: {
$apply: nodes => appendNewNodes(nodes, comments.nodes),
},
hasNextPage: { $set: comments.hasNextPage },
endCursor: { $set: comments.endCursor },
},
},
});
return updated;
},
});
};
render() {
const { currentUser, showSignInDialog, root, data } = this.props;
const { me } = this.props.root;
const loading = this.props.data.loading;
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (!currentUser) {
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
}
if (loading || !me) {
return <Spinner />;
}
const localProfile = currentUser.profiles.find(p => p.provider === 'local');
const emailAddress = localProfile && localProfile.id;
return (
<div className="talk-my-profile talk-embed-stream-profile-container">
<h2>{me.username}</h2>
{emailAddress ? <p>{emailAddress}</p> : null}
<Slot fill="profileSections" data={data} queryData={{ root }} />
<hr />
<h3>{t('framework.my_comments')}</h3>
<div
className={cn('talk-my-profile-comment-history', {
'talk-my-profile-comment-history-no-comments': !me.comments.nodes
.length,
})}
>
{me.comments.nodes.length ? (
<CommentHistory
data={data}
root={root}
comments={me.comments}
link={link}
loadMore={this.loadMore}
/>
) : (
<p className="talk-my-profile-comment-history-no-comments-cta">
{t('user_no_comment')}
</p>
)}
</div>
</div>
);
}
}
const slots = [
'profileSections',
// TODO: These Slots should be included in `talk-plugin-history` instead.
'commentContent',
'historyCommentTimestamp',
];
const CommentFragment = gql`
fragment TalkSettings_CommentConnectionFragment on CommentConnection {
nodes {
id
body
replyCount
action_summaries {
count
__typename
}
asset {
id
title
url
${getSlotFragmentSpreads(slots, 'asset')}
}
created_at
${getSlotFragmentSpreads(slots, 'comment')}
}
endCursor
hasNextPage
}
`;
const LOAD_MORE_QUERY = gql`
query TalkSettings_LoadMoreComments($limit: Int, $cursor: Cursor) {
me {
comments(query: { limit: $limit, cursor: $cursor }) {
...TalkSettings_CommentConnectionFragment
}
}
}
${CommentFragment}
`;
const withProfileQuery = withQuery(
gql`
query CoralEmbedStream_Profile {
me {
id
username
comments(query: {limit: 10}) {
...TalkSettings_CommentConnectionFragment
}
}
${getSlotFragmentSpreads(slots, 'root')}
}
${CommentFragment}
`,
{
options: {
fetchPolicy: 'network-only',
},
}
);
const mapStateToProps = state => ({
currentUser: state.auth.user,
});
const mapDispatchToProps = dispatch =>
bindActionCreators({ showSignInDialog }, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withProfileQuery
)(ProfileContainer);
@@ -4,7 +4,10 @@ import Comment from '../components/Comment';
import { withFragments } from 'coral-framework/hocs';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import { withSetCommentStatus } from 'coral-framework/graphql/mutations';
import { THREADING_LEVEL } from '../../../constants/stream';
import {
THREADING_LEVEL,
REPLY_COMMENTS_LOAD_DEPTH,
} from '../../../constants/stream';
import hoistStatics from 'recompose/hoistStatics';
import { nest } from '../../../graphql/utils';
@@ -118,7 +121,7 @@ const withCommentFragments = withFragments({
...CoralEmbedStream_Comment_SingleComment
${nest(
`
replies(query: {limit: 3, excludeIgnored: $excludeIgnored}) {
replies(query: {limit: ${REPLY_COMMENTS_LOAD_DEPTH}, excludeIgnored: $excludeIgnored}) {
nodes {
...CoralEmbedStream_Comment_SingleComment
...nest
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {
ADDTL_COMMENTS_ON_LOAD_MORE,
ASSET_COMMENTS_LOAD_DEPTH,
THREADING_LEVEL,
} from '../../../constants/stream';
import {
@@ -424,7 +425,7 @@ const fragments = {
requireEmailConfirmation
}
totalCommentCount @skip(if: $hasComment)
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
comments(query: {limit: ${ASSET_COMMENTS_LOAD_DEPTH}, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
nodes {
...CoralEmbedStream_Stream_comment
}
+1 -5
View File
@@ -1,9 +1,5 @@
import Pym from 'pym.js';
const pym = new Pym.Child({ polling: 100 });
export default pym;
export const link = url => e => {
e.preventDefault();
pym.sendMessage('navigate', url);
};
export default pym;
+1 -1
View File
@@ -21,7 +21,7 @@ export const getReliability = reliabilityValue => {
*/
export const isSuspended = user => {
const suspensionUntil = get(user, 'status.suspension.until');
const suspensionUntil = get(user, 'state.status.suspension.until');
return user && suspensionUntil && new Date(suspensionUntil) > new Date();
};
+1
View File
@@ -10,6 +10,7 @@
line-height: 22px;
min-width: 80px;
text-align: center;
vertical-align: text-top;
}
.icon {
+4
View File
@@ -44,6 +44,10 @@ const CONFIG = {
// fetching again.
SETTINGS_CACHE_TIME: ms(process.env.TALK_SETTINGS_CACHE_TIME || '1hr'),
// ALLOW_NO_LIMIT_QUERIES enables some queries to specify a limit of -1 to
// request all of the records. Otherwise, minimum limits of 0 are enforced.
ALLOW_NO_LIMIT_QUERIES: process.env.TALK_ALLOW_NO_LIMIT_QUERIES === 'TRUE',
//------------------------------------------------------------------------------
// JWT based configuration
//------------------------------------------------------------------------------
@@ -76,6 +76,30 @@ or by visiting the
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
enabled.
## TALK_GOOGLE_CLIENT_ID
The Google OAuth2 client ID for your Google login web app. You can learn more
about getting a Google Client ID at the
[Google API Console](https://console.developers.google.com/apis/){:target="_blank"}.
You will need to enable the Google+ API in the dashboard and create credentials
for a new OAuth client ID web application. The authorized JavaScript origin
should be set to the Talk domain, and the authorized redirect URI should be set
to http://<example.com>/api/v1/auth/google/callback. This is only required while
the `talk-plugin-google-auth` plugin is enabled.
## TALK_GOOGLE_CLIENT_SECRET
The Google OAuth2 client ID for your Google login web app. You can learn more
about getting a Google Client ID at the
[Google API Console](https://console.developers.google.com/apis/){:target="_blank"}.
You will need to enable the Google+ API in the dashboard and create credentials
for a new OAuth client ID web application. The authorized JavaScript origin
should be set to the Talk domain, and the authorized redirect URI should be set
to http://<example.com>/api/v1/auth/google/callback. This is only required while
the `talk-plugin-google-auth` plugin is enabled.
## TALK_HELMET_CONFIGURATION
A JSON string representing the configuration passed to the
@@ -501,3 +525,33 @@ Used to set the key for use with
tracing of GraphQL requests.
**Note: Apollo Engine is a premium service, charges may apply.**
## ALLOW_NO_LIMIT_QUERIES
Setting this to `TRUE` will allow queries to execute without a limit (returns
all documents). This introduces a significant performance regression, and should
be used with caution. (Default `FALSE`)
## TALK_ADDTL_COMMENTS_ON_LOAD_MORE
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
image you can specify it with `--build-arg TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10`.
Specifies the number of additional comments to load when a user clicks `Load More`. (Default `10`)
## TALK_ASSET_COMMENTS_LOAD_DEPTH
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
image you can specify it with `--build-arg TALK_ASSET_COMMENTS_LOAD_DEPTH=10`.
Specifies the initial number of comments to load for an asset. (Default `10`)
## TALK_REPLY_COMMENTS_LOAD_DEPTH
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
image you can specify it with `--build-arg TALK_REPLY_COMMENTS_LOAD_DEPTH=3`.
Specifies the initial replies to load for a comment. (Default `3`)
+158 -28
View File
@@ -1,54 +1,184 @@
const DataLoader = require('dataloader');
const util = require('./util');
const ActionsService = require('../../services/actions');
const ActionModel = require('../../models/action');
const { first, get, merge, remove, groupBy, reduce, isNil } = require('lodash');
/**
* Gets actions based on their item id's.
*/
const genActionsByItemID = (_, item_ids) => {
return ActionsService.findByItemIdArray(item_ids).then(
const genActionsByItemID = (
{ connectors: { services: { Actions } } },
item_ids
) => {
return Actions.findByItemIdArray(item_ids).then(
util.arrayJoinBy(item_ids, 'item_id')
);
};
/**
* Looks up actions based on the requested id's all bounded by the user.
* @param {Object} context the context of the request
* @param {Array} ids array of id's to get
* @return {Promise} resolves to the promises of the requested actions
* Looks up the actions for each of the items.
*
* @param {Object} ctx the graph context of the request
* @param {Array<String>} itemIDs the items that we need to get the actions for
*/
const genActionSummariessByItemID = ({ user = {} }, item_ids) => {
return ActionsService.getActionSummaries(item_ids, user.id).then(
util.arrayJoinBy(item_ids, 'item_id')
const genActionsAuthoredWithID = (
{ user = {}, connectors: { services: { Actions } } },
itemIDs
) =>
Actions.getUserActions(user.id, itemIDs).then(
util.arrayJoinBy(itemIDs, 'item_id')
);
};
/**
* Search for actions based on their action_type and item_type and ensures that
* the actions returned have unique item id's.
* @param {String} action_type the action to search by
* @param {String} item_type the item id to search by
* @return {Promise} resolves to distinct items actions
* iterateActionCounts will create an iterable object that can be used to
* compute action summaries.
*
* @param {Object} action_counts the action count object
*/
const getItemIdsByActionTypeAndItemType = (_, action_type, item_type) => {
return ActionModel.distinct('item_id', { action_type, item_type });
const iterateActionCounts = action_counts =>
!isNil(action_counts)
? Object.keys(action_counts).map(action_type => ({
count: action_counts[action_type],
action_type: action_type.toUpperCase(),
}))
: [];
/**
* getUserActions will get the actions made by the user for this specific
* item.
*
* @param {Object} ctx the graph context of the request
* @param {Object} item the item that we're getting the actions for
*/
async function getUserActions(ctx, { action_counts, id }) {
const { loaders: { Actions } } = ctx;
// Get the total count for all action types.
const totalActionCount = reduce(
action_counts,
(total, count) => total + count,
0
);
// Check to see if there are any user actions to get.
const hasUserActions = ctx.user && totalActionCount > 0;
if (!hasUserActions) {
return {};
}
// Possibly get the list of user actions completed by the user. This will be
// used later to join together with the action summaries to provide context.
const userActions = await Actions.getAuthoredByID.load(id);
if (userActions.length === 0) {
return {};
}
// Group the user actions in the same way that the action counts are
// grouped. This will let us extract it easy.
return reduce(
groupBy(userActions, ({ action_type, group_id }) =>
(group_id ? `${action_type}_${group_id}` : action_type).toUpperCase()
),
(allUserActions, userActions, actionType) =>
merge(allUserActions, { [actionType]: first(userActions) }),
{}
);
}
// This will match any action count that is specific for a group id.
const nonGroupIDTest = /^([A-Z]+)_([A-Z_]+)$/;
/**
* resolveActionSummariesForItem will resolve the action summaries for an item.
*
* @param {Object} ctx the graph context of the request
* @param {Object} item the item that we are resolving an action summary for
*/
async function resolveActionSummariesForItem(ctx, { id, action_counts }) {
// Cache all those entries for which we got the group id of, because we
// don't want to include them twice.
const groupIDCache = {};
// Get the user actions for this specific item.
const groupedUserActions = await getUserActions(ctx, { id, action_counts });
// Generate the action summaries for the item.
return iterateActionCounts(action_counts).reduce(
(actionTypeList, { count, action_type }) => {
// Get the current user's actions (if they have any).
const current_user = get(groupedUserActions, action_type, null);
// Check to see if this is a action without a corresponding group id.
if (nonGroupIDTest.test(action_type)) {
// This action type does have a group id associated with it.
const results = nonGroupIDTest.exec(action_type);
const groupActionType = results[1];
const groupID = results[2];
// Purge out the summary if it already exists, and mark that this
// group id has been found so we don't include it in the future.
remove(
actionTypeList,
({ action_type }) => action_type === groupActionType
);
groupIDCache[groupActionType] = true;
// Push the new entry in.
actionTypeList.push({
action_type: groupActionType,
group_id: groupID,
count,
current_user,
});
} else {
// This does not have a group id. Check to see if this group id
// already has an specific (group id) entry.
if (groupIDCache[action_type]) {
// It does. Don't add anything.
return actionTypeList;
}
// It does not, add the entry.
actionTypeList.push({
action_type,
group_id: null,
count,
current_user,
});
}
return actionTypeList;
},
[]
);
}
/**
* Looks up the action summaries for a set of items.
*
* @param {Object} ctx the graph context of the request
* @param {Array<Object>} items the items that should have their items looked up for
*/
const genActionSummariesByItem = async (ctx, items) => {
// This is designed to match the action_counts value that is embedded on
// documents which cache action counts. For users that are not logged in, we
// don't need to hit the actions collection at all!
// We will literate over all the items that we're comparing.
return items.map(item => resolveActionSummariesForItem(ctx, item));
};
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
* @param {Object} ctx the context of the GraphQL request
* @return {Object} object of loaders
*/
module.exports = context => ({
module.exports = ctx => ({
Actions: {
getByID: new DataLoader(ids => genActionsByItemID(context, ids)),
getSummariesByItemID: new DataLoader(ids =>
genActionSummariessByItemID(context, ids)
getByID: new DataLoader(ids => genActionsByItemID(ctx, ids)),
getSummariesByItem: new DataLoader(
items => genActionSummariesByItem(ctx, items),
{ cacheKeyFn: ({ id }) => id }
),
getByTypes: ({ action_type, item_type }) =>
getItemIdsByActionTypeAndItemType(context, action_type, item_type),
getAuthoredByID: new DataLoader(ids => genActionsAuthoredWithID(ctx, ids)),
},
});
+44 -43
View File
@@ -4,7 +4,10 @@ const {
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS,
SEARCH_OTHERS_COMMENTS,
} = require('../../perms/constants');
const { CACHE_EXPIRY_COMMENT_COUNT } = require('../../config');
const {
CACHE_EXPIRY_COMMENT_COUNT,
ALLOW_NO_LIMIT_QUERIES,
} = require('../../config');
const ms = require('ms');
const sc = require('snake-case');
@@ -148,12 +151,11 @@ const getCommentCountByQuery = (ctx, options) => {
* @param {Object} params the params from the client describing the query
*/
const getStartCursor = (ctx, nodes, { cursor, sortBy }) => {
switch (sortBy) {
case 'CREATED_AT':
return nodes.length ? nodes[0].created_at : null;
case 'REPLIES':
// The cursor is the start! This is using numeric pagination.
return cursor != null ? cursor : 0;
if (sortBy === 'CREATED_AT') {
return nodes.length ? nodes[0].created_at : null;
} else if (sortBy === 'REPLIES') {
// The cursor is the start! This is using numeric pagination.
return cursor != null ? cursor : 0;
}
const SORT_KEY = sortBy.toLowerCase();
@@ -181,11 +183,10 @@ const getStartCursor = (ctx, nodes, { cursor, sortBy }) => {
* @param {Object} params the params from the client describing the query
*/
const getEndCursor = (ctx, nodes, { cursor, sortBy }) => {
switch (sortBy) {
case 'CREATED_AT':
return nodes.length ? nodes[nodes.length - 1].created_at : null;
case 'REPLIES':
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
if (sortBy === 'CREATED_AT') {
return nodes.length ? nodes[nodes.length - 1].created_at : null;
} else if (sortBy === 'REPLIES') {
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
}
const SORT_KEY = sortBy.toLowerCase();
@@ -212,36 +213,33 @@ const getEndCursor = (ctx, nodes, { cursor, sortBy }) => {
* @param {Object} params the params from the client describing the query
*/
const applySort = (ctx, query, { cursor, sortOrder, sortBy }) => {
switch (sortBy) {
case 'CREATED_AT': {
if (cursor) {
if (sortOrder === 'DESC') {
query = query.where({
created_at: {
$lt: cursor,
},
});
} else {
query = query.where({
created_at: {
$gt: cursor,
},
});
}
if (sortBy === 'CREATED_AT') {
if (cursor) {
if (sortOrder === 'DESC') {
query = query.where({
created_at: {
$lt: cursor,
},
});
} else {
query = query.where({
created_at: {
$gt: cursor,
},
});
}
return query.sort({ created_at: sortOrder === 'DESC' ? -1 : 1 });
}
case 'REPLIES': {
if (cursor) {
query = query.skip(cursor);
}
return query.sort({
reply_count: sortOrder === 'DESC' ? -1 : 1,
created_at: sortOrder === 'DESC' ? -1 : 1,
});
return query.sort({ created_at: sortOrder === 'DESC' ? -1 : 1 });
} else if (sortBy === 'REPLIES') {
if (cursor) {
query = query.skip(cursor);
}
return query.sort({
reply_count: sortOrder === 'DESC' ? -1 : 1,
created_at: sortOrder === 'DESC' ? -1 : 1,
});
}
const SORT_KEY = sortBy.toLowerCase();
@@ -280,7 +278,7 @@ const executeWithSort = async (
query = applySort(ctx, query, { cursor, sortOrder, sortBy });
// Apply the limit (if it exists, as it's applied universally).
if (limit) {
if (limit >= 0) {
query = query.limit(limit + 1);
}
@@ -290,7 +288,7 @@ const executeWithSort = async (
// The hasNextPage is always handled the same (ask for one more than we need,
// if there is one more, than there is more).
let hasNextPage = false;
if (limit && nodes.length > limit) {
if (limit >= 0 && nodes.length > limit) {
// There was one more than we expected! Set hasNextPage = true and remove
// the last item from the array that we requested.
hasNextPage = true;
@@ -302,11 +300,9 @@ const executeWithSort = async (
return {
startCursor: getStartCursor(ctx, nodes, {
cursor,
sortOrder,
sortBy,
limit,
}),
endCursor: getEndCursor(ctx, nodes, { cursor, sortOrder, sortBy, limit }),
endCursor: getEndCursor(ctx, nodes, { cursor, sortBy }),
hasNextPage,
nodes,
};
@@ -338,6 +334,11 @@ const getCommentsByQuery = async (
) => {
let comments = CommentModel.find();
// Enforce that the limit must be gte 0 if this option is not true.
if (!ALLOW_NO_LIMIT_QUERIES && limit < 0) {
throw new Error('cannot query for limit < 0');
}
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
// special privileges.
if (
+4 -4
View File
@@ -40,12 +40,12 @@ const Comment = {
return Actions.getByID.load(id);
},
action_summaries({ id, action_summaries }, _, { loaders: { Actions } }) {
if (action_summaries) {
return action_summaries;
action_summaries(comment, _, { loaders: { Actions } }) {
if (comment.action_summaries) {
return comment.action_summaries;
}
return Actions.getSummariesByItemID.load(id);
return Actions.getSummariesByItem.load(comment);
},
asset({ asset_id }, _, { loaders: { Assets } }) {
return Assets.getByID.load(asset_id);
+2 -2
View File
@@ -10,8 +10,8 @@ const {
} = require('../../perms/constants');
const User = {
action_summaries({ id }, _, { loaders: { Actions } }) {
return Actions.getSummariesByItemID.load(id);
action_summaries(user, _, { loaders: { Actions } }) {
return Actions.getSummariesByItem.load(user);
},
actions({ id }, _, { user, loaders: { Actions } }) {
// Only return the actions if the user is not an admin.
+6 -4
View File
@@ -1,11 +1,10 @@
{
"name": "talk",
"version": "4.2.0",
"version": "4.2.2",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"private": true,
"scripts": {
"postinstall": "./bin/cli plugins reconcile --skip-remote",
"generate-introspection": "WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js",
"clean": "rm -rf dist client/coral-framework/graphql/introspection.json",
"watch": "npm-run-all clean generate-introspection --parallel watch:*",
@@ -32,6 +31,9 @@
"minVersion": 1516920160
}
},
"workspaces": [
"plugins/*"
],
"repository": {
"type": "git",
"url": "git+https://github.com/coralproject/talk.git"
@@ -117,9 +119,9 @@
"inquirer": "^3.2.2",
"inquirer-autocomplete-prompt": "^0.12.1",
"ioredis": "3.1.4",
"joi": "^10.6.0",
"joi": "^13.0.0",
"json-loader": "^0.5.7",
"jsonwebtoken": "^7.4.3",
"jsonwebtoken": "^8.0.0",
"jwt-decode": "^2.2.0",
"keymaster": "^1.6.2",
"kue": "0.11.6",
+2 -1
View File
@@ -21,6 +21,7 @@
"talk-plugin-sort-most-respected",
"talk-plugin-sort-newest",
"talk-plugin-sort-oldest",
"talk-plugin-viewing-options"
"talk-plugin-viewing-options",
"talk-plugin-profile-settings"
]
}
@@ -5,7 +5,7 @@ import { t } from 'plugin-api/beta/client/services';
const SpamLabel = () => (
<CommentDetail
icon={'add_box'}
icon={'bug_report'}
header={t('talk-plugin-akismet.spam_comment')}
info={t('talk-plugin-akismet.detected')}
/>
@@ -3,7 +3,7 @@ import { FlagLabel } from 'plugin-api/beta/client/components/ui';
import { t } from 'plugin-api/beta/client/services';
const SpamLabel = () => (
<FlagLabel iconName="add_box">{t('talk-plugin-akismet.spam')}</FlagLabel>
<FlagLabel iconName="bug_report">{t('talk-plugin-akismet.spam')}</FlagLabel>
);
export default SpamLabel;
@@ -5,7 +5,7 @@ import { isSpam } from '../utils';
const enhance = compose(
excludeIf(
({ comment: { spam, actions } }) => spam === null || isSpam(actions)
({ comment: { spam, actions } }) => spam === null || !isSpam(actions)
)
);
@@ -1,12 +0,0 @@
import { compose } from 'react-apollo';
import { excludeIf } from 'plugin-api/beta/client/hocs';
import SpamDetail from './SpamDetail';
import { isSpam } from '../utils';
const enhance = compose(
excludeIf(
({ comment: { spam, actions } }) => spam === null || !isSpam(actions)
)
);
export default enhance(SpamDetail);
@@ -2,7 +2,6 @@ import translations from './translations.yml';
import CheckSpamHook from './containers/CheckSpamHook';
import SpamLabel from './containers/SpamLabel';
import SpamCommentDetail from './containers/SpamCommentDetail';
import SpamCommentFlagDetail from './containers/SpamCommentFlagDetail';
export default {
translations,
@@ -10,6 +9,5 @@ export default {
commentInputDetailArea: [CheckSpamHook],
adminCommentLabels: [SpamLabel],
adminCommentMoreDetails: [SpamCommentDetail],
adminCommentMoreFlagDetails: [SpamCommentFlagDetail],
},
};
@@ -68,7 +68,7 @@ nl_NL:
spam_comment: "Spam"
detected: "Gedetecteerd door Akismet"
still_spam: |
Dank je wel. Ons moderatieteam zal je reactie beoordelen.
Dank je wel. Ons moderatieteam zal je reactie beoordelen.
flags:
reasons:
comment:
-141
View File
@@ -1,141 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
akismet-api@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/akismet-api/-/akismet-api-4.0.1.tgz#1c771442f09316847132aa16171bb4fb708b6519"
dependencies:
bluebird "^3.1.1"
superagent "^3.8.0"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
bluebird@^3.1.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
combined-stream@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
dependencies:
delayed-stream "~1.0.0"
component-emitter@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
cookiejar@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a"
core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
debug@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
dependencies:
ms "2.0.0"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
extend@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
form-data@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf"
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.5"
mime-types "^2.1.12"
formidable@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9"
inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
methods@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
mime-db@~1.30.0:
version "1.30.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
mime-types@^2.1.12:
version "2.1.17"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
dependencies:
mime-db "~1.30.0"
mime@^1.4.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
qs@^6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
readable-stream@^2.0.5:
version "2.3.3"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~1.0.6"
safe-buffer "~5.1.1"
string_decoder "~1.0.3"
util-deprecate "~1.0.1"
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
string_decoder@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
dependencies:
safe-buffer "~5.1.0"
superagent@^3.8.0:
version "3.8.2"
resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403"
dependencies:
component-emitter "^1.2.0"
cookiejar "^2.1.0"
debug "^3.1.0"
extend "^3.0.0"
form-data "^2.3.1"
formidable "^1.1.1"
methods "^1.1.1"
mime "^1.4.1"
qs "^6.5.1"
readable-stream "^2.0.5"
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -73,7 +73,7 @@ ForgotPassword.propTypes = {
email: PropTypes.string.isRequired,
onEmailChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
onSignInLink: PropTypes.func.isRequired,
onSignUpLink: PropTypes.func.isRequired,
};
@@ -52,7 +52,7 @@ ResendVerification.propTypes = {
loading: PropTypes.bool.isRequired,
email: PropTypes.string.isRequired,
onSubmit: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
};
export default ResendVerification;
@@ -129,7 +129,7 @@ SignIn.propTypes = {
onSignUpLink: PropTypes.func.isRequired,
onRecaptchaVerify: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
requireRecaptcha: PropTypes.bool.isRequired,
};
@@ -145,20 +145,20 @@ class SignUp extends React.Component {
SignUp.propTypes = {
loading: PropTypes.bool.isRequired,
username: PropTypes.string.isRequired,
usernameError: PropTypes.string.isRequired,
usernameError: PropTypes.string,
email: PropTypes.string.isRequired,
emailError: PropTypes.string.isRequired,
emailError: PropTypes.string,
password: PropTypes.string.isRequired,
passwordError: PropTypes.string.isRequired,
passwordError: PropTypes.string,
passwordRepeat: PropTypes.string.isRequired,
passwordRepeatError: PropTypes.string.isRequired,
passwordRepeatError: PropTypes.string,
onUsernameChange: PropTypes.func.isRequired,
onEmailChange: PropTypes.func.isRequired,
onPasswordChange: PropTypes.func.isRequired,
onPasswordRepeatChange: PropTypes.func.isRequired,
onSignInLink: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
requireEmailConfirmation: PropTypes.bool.isRequired,
success: PropTypes.bool.isRequired,
};
@@ -38,7 +38,7 @@ class ForgotPasswordContainer extends Component {
ForgotPasswordContainer.propTypes = {
success: PropTypes.bool.isRequired,
forgotPassword: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
setView: PropTypes.func.isRequired,
email: PropTypes.string.isRequired,
setEmail: PropTypes.func.isRequired,
@@ -61,7 +61,7 @@ class SignInContainer extends Component {
SignInContainer.propTypes = {
signIn: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
requireRecaptcha: PropTypes.bool.isRequired,
requireEmailConfirmation: PropTypes.bool.isRequired,
loading: PropTypes.bool.isRequired,
@@ -109,7 +109,7 @@ SignUpContainer.propTypes = {
setPassword: PropTypes.func.isRequired,
signUp: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
requireEmailConfirmation: PropTypes.bool.isRequired,
success: PropTypes.bool.isRequired,
validate: PropTypes.func.isRequired,
@@ -75,10 +75,10 @@ class SetUsernameDialog extends React.Component {
SetUsernameDialog.propTypes = {
loading: PropTypes.bool.isRequired,
username: PropTypes.string.isRequired,
usernameError: PropTypes.string.isRequired,
usernameError: PropTypes.string,
onUsernameChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
};
export default SetUsernameDialog;
@@ -47,7 +47,7 @@ SetUsernameDialogContainer.propTypes = {
username: PropTypes.string,
setUsername: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
errorMessage: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
success: PropTypes.bool.isRequired,
validateUsername: PropTypes.func.isRequired,
};
@@ -1,112 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
asap@~2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f"
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
encoding@^0.1.11:
version "0.1.12"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
dependencies:
iconv-lite "~0.4.13"
fbjs@^0.8.9:
version "0.8.12"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04"
dependencies:
core-js "^1.0.0"
isomorphic-fetch "^2.1.1"
loose-envify "^1.0.0"
object-assign "^4.1.0"
promise "^7.1.1"
setimmediate "^1.0.5"
ua-parser-js "^0.7.9"
iconv-lite@~0.4.13:
version "0.4.18"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
is-stream@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
isomorphic-fetch@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
dependencies:
node-fetch "^1.0.1"
whatwg-fetch ">=0.10.0"
js-tokens@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
linkify-it@^1.2.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-1.2.4.tgz#0773526c317c8fd13bd534ee1d180ff88abf881a"
dependencies:
uc.micro "^1.0.1"
loose-envify@^1.0.0, loose-envify@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
dependencies:
js-tokens "^3.0.0"
node-fetch@^1.0.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.1.tgz#899cb3d0a3c92f952c47f1b876f4c8aeabd400d5"
dependencies:
encoding "^0.1.11"
is-stream "^1.0.1"
object-assign@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
promise@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf"
dependencies:
asap "~2.0.3"
prop-types@^15.5.8:
version "15.5.10"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"
dependencies:
fbjs "^0.8.9"
loose-envify "^1.3.1"
react-linkify@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/react-linkify/-/react-linkify-0.2.1.tgz#b28d3f9544539a622fec8d42b4800eb9d23bf981"
dependencies:
linkify-it "^1.2.0"
prop-types "^15.5.8"
tlds "^1.57.0"
setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
tlds@^1.57.0:
version "1.189.0"
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.189.0.tgz#b8cb46ea76dc2f4a01d45b8d907bf19a66e9f729"
ua-parser-js@^0.7.9:
version "0.7.12"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb"
uc.micro@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"
whatwg-fetch@>=0.10.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
@@ -4,17 +4,21 @@ en:
sign_up: "Sign up with Facebook"
es:
talk-plugin-facebook-auth:
facebook_sign_in: "Entrar con Facebook"
facebook_sign_up: "Registrarse con Facebook"
sign_in: "Entrar con Facebook"
sign_up: "Registrarse con Facebook"
fr:
talk-plugin-facebook-auth:
facebook_sign_in: "Connectez-vous avec Facebook"
facebook_sign_up: "Inscrivez-vous avec Facebook"
sign_in: "Connectez-vous avec Facebook"
sign_up: "Inscrivez-vous avec Facebook"
zh_CN:
talk-plugin-facebook-auth:
facebook_sign_in: "使用 Facebook 帐号"
facebook_sign_up: "使用 Facebook 帐号"
sign_in: "使用 Facebook 帐号"
sign_up: "使用 Facebook 帐号"
zh_TW:
talk-plugin-facebook-auth:
facebook_sign_in: "使用 Facebook 帳號"
facebook_sign_up: "使用 Facebook 帳號"
sign_in: "使用 Facebook 帳號"
sign_up: "使用 Facebook 帳號"
de:
talk-plugin-facebook-auth:
sign_in: "Mit Facebook anmelden"
sign_up: "Mit Facebook registrieren"
@@ -1,34 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
oauth@0.9.x:
version "0.9.15"
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
passport-facebook@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/passport-facebook/-/passport-facebook-2.1.1.tgz#c39d0b52ae4d59163245a4e21a7b9b6321303311"
dependencies:
passport-oauth2 "1.x.x"
passport-oauth2@1.x.x:
version "1.4.0"
resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.4.0.tgz#f62f81583cbe12609be7ce6f160b9395a27b86ad"
dependencies:
oauth "0.9.x"
passport-strategy "1.x.x"
uid2 "0.0.x"
utils-merge "1.x.x"
passport-strategy@1.x.x:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
uid2@0.0.x:
version "0.0.3"
resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82"
utils-merge@1.x.x:
version "1.0.0"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8"
@@ -1,4 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk"
}
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk/client"
}
@@ -0,0 +1,7 @@
export const loginWithGoogle = () => (dispatch, _, { rest }) => {
window.open(
`${rest.uri}/auth/google`,
'Continue with Google',
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
);
};
@@ -0,0 +1,13 @@
.button {
background-color: #db3236;
border-color: #db3236;
color: rgb(255, 255, 255);
width: 100%;
box-sizing: border-box;
padding: 10px 20px;
}
.button:hover {
background-color: #c71e22;
border-color: #c71e22;
}
@@ -0,0 +1,11 @@
import React from 'react';
import { BareButton } from 'plugin-api/beta/client/components/ui';
import styles from './GoogleButton.css';
export default ({ onClick, children }) => {
return (
<BareButton className={styles.button} onClick={onClick}>
{children}
</BareButton>
);
};
@@ -0,0 +1,9 @@
import React from 'react';
import GoogleButton from '../containers/GoogleButton';
import { t } from 'plugin-api/beta/client/services';
export default () => {
return (
<GoogleButton>{t('talk-plugin-google-auth.sign_in')}</GoogleButton>
);
};
@@ -0,0 +1,9 @@
import React from 'react';
import GoogleButton from '../containers/GoogleButton';
import { t } from 'plugin-api/beta/client/services';
export default () => {
return (
<GoogleButton>{t('talk-plugin-google-auth.sign_up')}</GoogleButton>
);
};
@@ -0,0 +1,9 @@
import { connect } from 'plugin-api/beta/client/hocs';
import { bindActionCreators } from 'redux';
import { loginWithGoogle } from '../actions';
import GoogleButton from '../components/GoogleButton';
const mapDispatchToProps = dispatch =>
bindActionCreators({ onClick: loginWithGoogle }, dispatch);
export default connect(null, mapDispatchToProps)(GoogleButton);
@@ -0,0 +1,11 @@
import SignIn from './components/SignIn';
import SignUp from './components/SignUp';
import translations from './translations.yml';
export default {
translations,
slots: {
authExternalSignIn: [SignIn],
authExternalSignUp: [SignUp],
},
};
@@ -0,0 +1,20 @@
en:
talk-plugin-google-auth:
sign_in: "Sign in with Google"
sign_up: "Sign up with Google"
es:
talk-plugin-google-auth:
google_sign_in: "Entrar con Google"
google_sign_up: "Registrarse con Google"
fr:
talk-plugin-google-auth:
google_sign_in: "Connectez-vous avec Google"
google_sign_up: "Inscrivez-vous avec Google"
zh_CN:
talk-plugin-google-auth:
google_sign_in: "使用 Google 帐号"
google_sign_up: "使用 Google 帐号"
zh_TW:
talk-plugin-google-auth:
google_sign_in: "使用 Google 帳號"
google_sign_up: "使用 Google 帳號"
+7
View File
@@ -0,0 +1,7 @@
const passport = require('./server/passport');
const router = require('./server/router');
module.exports = {
passport,
router,
};
@@ -0,0 +1,9 @@
{
"name": "talk-plugin-google-auth",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"passport-google-oauth2": "^0.1.6"
}
}
@@ -0,0 +1,41 @@
const GoogleStrategy = require('passport-google-oauth2').Strategy;
const UsersService = require('services/users');
const { ValidateUserLogin } = require('services/passport');
let { ROOT_URL } = require('config');
if (ROOT_URL[ROOT_URL.length - 1] !== '/') {
ROOT_URL += '/';
}
module.exports = passport => {
if (
process.env.TALK_GOOGLE_CLIENT_ID &&
process.env.TALK_GOOGLE_CLIENT_SECRET &&
process.env.TALK_ROOT_URL
) {
passport.use(
new GoogleStrategy(
{
clientID: process.env.TALK_GOOGLE_CLIENT_ID,
clientSecret: process.env.TALK_GOOGLE_CLIENT_SECRET,
callbackURL: `${ROOT_URL}api/v1/auth/google/callback`,
passReqToCallback: true,
},
async (req, accessToken, refreshToken, profile, done) => {
let user;
try {
user = await UsersService.findOrCreateExternalUser(profile);
} catch (err) {
return done(err.toString());
}
return ValidateUserLogin(profile, user, done);
}
)
);
} else if (process.env.NODE_ENV !== 'test') {
throw new Error(
'Google cannot be enabled, missing one of TALK_GOOGLE_CLIENT_ID, TALK_GOOGLE_CLIENT_SECRET, TALK_ROOT_URL'
);
}
};
@@ -0,0 +1,29 @@
module.exports = router => {
const { passport, HandleAuthPopupCallback } = require('services/passport');
/**
* Google auth endpoint, this will redirect the user immediatly to google
* for authorization.
*/
router.get(
'/api/v1/auth/google',
passport.authenticate('google', {
display: 'popup',
authType: 'rerequest',
scope: ['profile'],
})
);
/**
* Google callback endpoint, this will send the user a html page designed to
* send back the user credentials upon sucesfull login.
*/
router.get('/api/v1/auth/google/callback', (req, res, next) => {
// Perform the google login flow and pass the data back through the opener.
passport.authenticate(
'google',
{ session: false },
HandleAuthPopupCallback(req, res, next)
)(req, res, next);
});
};
@@ -8,7 +8,7 @@ export default {
slots: {
authorMenuActions: [IgnoreUserAction],
ignoreUserConfirmation: [IgnoreUserConfirmation],
profileSections: [IgnoredUserSection],
profileSettings: [IgnoredUserSection],
},
translations,
mutations: {
-4
View File
@@ -1,4 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk"
}
@@ -0,0 +1,3 @@
{
"extends": "@coralproject/eslint-config-talk/client"
}
@@ -0,0 +1,8 @@
import React from 'react';
import { t } from 'plugin-api/beta/client/services';
const Tab = () => {
return <span>{t('talk-plugin-profile-settings.tab')}</span>;
};
export default Tab;
@@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Slot } from 'plugin-api/beta/client/components';
class TabPane extends React.Component {
render() {
const { data, root } = this.props;
return (
<div>
<Slot fill="profileSettings" data={data} queryData={{ root }} />
</div>
);
}
}
TabPane.propTypes = {
data: PropTypes.object,
root: PropTypes.object,
};
export default TabPane;
@@ -0,0 +1,26 @@
import React from 'react';
import { compose, gql } from 'react-apollo';
import TabPane from '../components/TabPane';
import { withFragments } from 'plugin-api/beta/client/hocs';
import { getSlotFragmentSpreads } from 'plugin-api/beta/client/utils';
const slots = ['profileSettings'];
class TabPaneContainer extends React.Component {
render() {
return <TabPane {...this.props} />;
}
}
const enhance = compose(
withFragments({
root: gql`
fragment TalkProfileSettings_TabPane_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}
`,
})
);
export default enhance(TabPaneContainer);
@@ -0,0 +1,11 @@
import Tab from './components/Tab';
import TabPane from './containers/TabPane';
import translations from './translations.yml';
export default {
slots: {
profileTabs: [Tab],
profileTabPanes: [TabPane],
},
translations,
};
@@ -0,0 +1,27 @@
en:
talk-plugin-profile-settings:
tab: Settings
de:
talk-plugin-profile-settings:
tab: Einstellungen
es:
talk-plugin-profile-settings:
tab: Configuración
fr:
talk-plugin-profile-settings:
tab: Paramètres
nl_NL:
talk-plugin-profile-settings:
tab: Instellingen
da:
talk-plugin-profile-settings:
tab: Indstillinger
pt_PR:
talk-plugin-profile-settings:
tab: Configurações
zh_TW:
talk-plugin-profile-settings:
tab: 設置
zh_CN:
talk-plugin-profile-settings:
tab: 设置
@@ -0,0 +1 @@
module.exports = {};
-11
View File
@@ -1,11 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
moment@^2.18.1:
version "2.18.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
momentjs@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/momentjs/-/momentjs-2.0.0.tgz#73df904b4fa418f6e3c605e831cef6ed5518ebd4"
@@ -29,7 +29,7 @@ const getInfo = (toxicity, actions) => {
const ToxicLabel = ({ comment: { actions, toxicity } }) => (
<CommentDetail
icon={'add_box'}
icon={'error'}
header={t('talk-plugin-toxic-comments.toxic_comment')}
info={getInfo(toxicity, actions)}
/>
@@ -1,6 +1,6 @@
import React from 'react';
import { FlagLabel } from 'plugin-api/beta/client/components/ui';
const ToxicLabel = () => <FlagLabel iconName="add_box">Toxic</FlagLabel>;
const ToxicLabel = () => <FlagLabel iconName="error">Toxic</FlagLabel>;
export default ToxicLabel;
@@ -1,7 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
ms@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+9 -100
View File
@@ -104,110 +104,19 @@ module.exports = class ActionsService {
}
/**
* Fetches the action summaries for the given asset, and comments around the
* given user id.
* Get the actions for a specific user on the specific items.
*
* @param {[type]} asset_id [description]
* @param {[type]} comments [description]
* @param {String} [current_user_id=''] [description]
* @return {[type]} [description]
* @param {String} userID the id of the user to find their actions for
* @param {Array<String>} itemIDs the ids of the items to find their actions
* for
*/
static getActionSummariesFromComments(
asset_id = '',
comments,
current_user_id = ''
) {
// Get the user id's from the author id's as a unique array that gets
// sorted.
let userIDs = _.uniq(comments.map(comment => comment.author_id)).sort();
// Fetch the actions for pretty much everything at this point.
return ActionsService.getActionSummaries(
_.uniq(
[
// Actions can be on assets...
asset_id,
// Comments...
...comments.map(comment => comment.id),
// Or Authors...
...userIDs,
].filter(e => e)
),
current_user_id
);
}
/**
* Returns summaries of actions for an array of ids.
*
* @param {String} ids array of user identifiers (uuid)
*/
static getActionSummaries(item_ids, current_user_id = '') {
// only grab items that match the specified item id's
let $match = {
static getUserActions(userID, itemIDs) {
return ActionModel.find({
user_id: userID,
item_id: {
$in: item_ids,
$in: itemIDs,
},
};
let $group = {
// group unique documents by these properties, we are leveraging the
// fact that each uuid is completely unique.
_id: {
item_id: '$item_id',
action_type: '$action_type',
group_id: '$group_id',
},
// and sum up all actions matching the above grouping criteria
count: {
$sum: 1,
},
// we are leveraging the fact that each uuid is completely unique and
// just grabbing the last instance of the item type here.
item_type: {
$first: '$item_type',
},
current_user: {
$max: {
$cond: {
if: {
$eq: ['$user_id', current_user_id],
},
then: '$$CURRENT',
else: null,
},
},
},
};
let $project = {
// suppress the _id field
_id: false,
// map the fields from the _id grouping down a level
item_id: '$_id.item_id',
action_type: '$_id.action_type',
group_id: '$_id.group_id',
// map the field directly
count: '$count',
item_type: '$item_type',
// set the current user to false here
current_user: '$current_user',
};
return ActionModel.aggregate([
{ $match },
{ $group },
{ $project },
{ $sort: { action_type: 1, group_id: 1 } },
]);
});
}
/**
+12 -6
View File
@@ -1,5 +1,5 @@
const jwt = require('jsonwebtoken');
const uniq = require('lodash/uniq');
const { merge, uniq, omitBy, isUndefined } = require('lodash');
/**
* MultiSecret will take many secrets and provide a unified interface for
@@ -22,7 +22,10 @@ class MultiSecret {
* Sign will sign with the first secret.
*/
sign(payload, options) {
return this.secrets[0].sign(payload, options);
return this.secrets[0].sign(
omitBy(payload, isUndefined),
omitBy(options, isUndefined)
);
}
/**
@@ -78,10 +81,13 @@ class Secret {
return jwt.sign(
payload,
this.signingKey,
Object.assign({}, options, {
keyid: this.kid,
algorithm: this.algorithm,
})
omitBy(
merge({}, options, {
keyid: this.kid,
algorithm: this.algorithm,
}),
isUndefined
)
);
}
+3 -1
View File
@@ -27,7 +27,9 @@ module.exports = class TokenService {
pat: true,
};
set(payload, JWT_USER_ID_CLAIM, userID);
if (userID) {
set(payload, JWT_USER_ID_CLAIM, userID);
}
// Sign the payload.
const jwt = JWT_SECRET.sign(payload, {});
+122
View File
@@ -0,0 +1,122 @@
const chai = require('chai');
chai.use(require('chai-as-promised'));
const { expect } = chai;
const sinon = require('sinon');
const { find } = require('lodash');
const loaders = require('../../../../graph/loaders/actions');
describe('graph.loaders.Actions', () => {
describe('#getAuthoredByID', () => {
it('loads the correct entries', async () => {
const spy = sinon.spy(async () => [
{ item_id: 'comment_1' },
{ item_id: 'comment_2' },
]);
const { Actions: { getAuthoredByID } } = loaders({
user: { id: 'user_1' },
connectors: { services: { Actions: { getUserActions: spy } } },
});
const actions = await getAuthoredByID.loadMany([
'comment_2',
'comment_1',
]);
expect(spy.calledWith('user_1', ['comment_2', 'comment_1']));
expect(actions).to.have.length(2);
expect(actions[0]).to.have.length(1);
expect(actions[0][0]).to.have.property('item_id', 'comment_2');
expect(actions[1]).to.have.length(1);
expect(actions[1][0]).to.have.property('item_id', 'comment_1');
});
});
describe('#getSummariesByItem', () => {
describe('logged out user', () => {
it('does not include any user data', async () => {
const { Actions: { getSummariesByItem } } = loaders({
loaders: {
Actions: {
getAuthoredByID: {
load: () => Promise.reject(new Error('should not be called')),
},
},
},
user: null,
});
const summaries = await getSummariesByItem.load({
id: '1',
action_counts: { flag: 1, flag_comment_offensive: 1, respect: 2 },
});
expect(summaries).to.have.length(2);
const flag = find(summaries, { action_type: 'FLAG' });
expect(flag).to.be.defined;
expect(flag).to.have.property('current_user', null);
expect(flag).to.have.property('action_type', 'FLAG');
expect(flag).to.have.property('group_id', 'COMMENT_OFFENSIVE');
expect(flag).to.have.property('count', 1);
const respect = find(summaries, { action_type: 'RESPECT' });
expect(respect).to.be.defined;
expect(respect).to.have.property('current_user', null);
expect(respect).to.have.property('action_type', 'RESPECT');
expect(respect).to.have.property('group_id', null);
expect(respect).to.have.property('count', 2);
});
});
describe('logged in user', () => {
it('does include user', async () => {
const { Actions: { getSummariesByItem } } = loaders({
loaders: {
Actions: {
getAuthoredByID: {
load: commentID => {
expect(commentID).to.equal('comment_1');
return [
{
id: 'action_1',
action_type: 'FLAG',
group_id: 'COMMENT_OFFENSIVE',
},
];
},
},
},
},
user: { id: 'user_1' },
});
const summaries = await getSummariesByItem.load({
id: 'comment_1',
action_counts: { flag: 1, flag_comment_offensive: 1, respect: 2 },
});
expect(summaries).to.have.length(2);
const flag = find(summaries, { action_type: 'FLAG' });
expect(flag).to.be.defined;
expect(flag).to.have.property('action_type', 'FLAG');
expect(flag).to.have.property('group_id', 'COMMENT_OFFENSIVE');
expect(flag).to.have.property('count', 1);
expect(flag).to.have.property('current_user').not.null;
expect(flag.current_user).to.have.property('id', 'action_1');
const respect = find(summaries, { action_type: 'RESPECT' });
expect(respect).to.be.defined;
expect(respect).to.have.property('current_user', null);
expect(respect).to.have.property('action_type', 'RESPECT');
expect(respect).to.have.property('group_id', null);
expect(respect).to.have.property('count', 2);
});
});
});
});
-63
View File
@@ -151,67 +151,4 @@ describe('services.ActionsService', () => {
);
});
});
describe('#getActionSummaries()', () => {
it('should return properly formatted summaries from an array of item_ids', () => {
return ActionsService.getActionSummaries([comment.id, '789']).then(
summaries => {
expect(summaries).to.have.length(2);
expect(summaries).to.deep.include({
action_type: 'LIKE',
count: 1,
item_id: comment.id,
item_type: 'COMMENTS',
current_user: null,
});
expect(summaries).to.deep.include({
action_type: 'FLAG',
count: 2,
item_id: comment.id,
item_type: 'COMMENTS',
current_user: null,
});
}
);
});
it('should include a current user when one is passed', () => {
return ActionsService.getActionSummaries(
[comment.id],
'flagginguserid'
).then(summaries => {
expect(summaries).to.have.length(2);
let summary = summaries.find(
s => s.item_id === comment.id && s.action_type === 'FLAG'
);
expect(summary).to.not.be.undefined;
expect(summary.current_user).to.not.be.null;
expect(summary.current_user).to.have.property('item_id', comment.id);
expect(summary.current_user).to.have.property('item_type', 'COMMENTS');
expect(summary.current_user).to.have.property(
'user_id',
'flagginguserid'
);
expect(summary.current_user).to.have.property('action_type', 'FLAG');
});
});
it("should not include a current user when one is passed for a user that doesn't have an action", () => {
return ActionsService.getActionSummaries(
[comment.id],
'flagginguserid2'
).then(summaries => {
expect(summaries).to.have.length(2);
summaries.forEach(summary => {
expect(summary).to.not.be.undefined;
expect(summary).to.have.property('current_user', null);
});
});
});
});
});
+1 -1
View File
@@ -5,7 +5,7 @@
<title>Email Verification</title>
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
<link rel="stylesheet" href="<%= BASE_PATH %>public/css/admin.css">
<%- include partials/head %>
<%- include ../partials/head %>
</head>
<body class="confirm-email-page">
<div id="root">
+1 -1
View File
@@ -5,7 +5,7 @@
<title>Password Reset</title>
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
<link rel="stylesheet" href="<%= BASE_PATH %>public/css/admin.css">
<%- include partials/head %>
<%- include ../partials/head %>
</head>
<body class="password-reset-page">
<div id="root">
+3
View File
@@ -128,6 +128,9 @@ const config = {
new webpack.EnvironmentPlugin({
TALK_PLUGINS_JSON: '{}',
TALK_THREADING_LEVEL: '3',
TALK_ADDTL_COMMENTS_ON_LOAD_MORE: '10',
TALK_ASSET_COMMENTS_LOAD_DEPTH: '10',
TALK_REPLY_COMMENTS_LOAD_DEPTH: '3',
TALK_DEFAULT_STREAM_TAB: 'all',
TALK_DEFAULT_LANG: 'en',
}),
+125 -26
View File
@@ -200,6 +200,13 @@ ajv@^5.1.0, ajv@^5.1.5, ajv@^5.2.3, ajv@^5.3.0:
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0"
akismet-api@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/akismet-api/-/akismet-api-4.0.1.tgz#1c771442f09316847132aa16171bb4fb708b6519"
dependencies:
bluebird "^3.1.1"
superagent "^3.8.0"
align-text@^0.1.1, align-text@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
@@ -1237,7 +1244,7 @@ bluebird@3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
bluebird@^3.0.6, bluebird@^3.3.4, bluebird@^3.4.6, bluebird@^3.5.0:
bluebird@^3.0.6, bluebird@^3.1.1, bluebird@^3.3.4, bluebird@^3.4.6, bluebird@^3.5.0:
version "3.5.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
@@ -2031,6 +2038,10 @@ cookiejar@2.0.x, cookiejar@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.0.6.tgz#0abf356ad00d1c5a219d88d44518046dd026acfe"
cookiejar@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a"
copy-concurrently@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
@@ -3397,7 +3408,7 @@ formatio@1.2.0, formatio@^1.2.0:
dependencies:
samsam "1.x"
formidable@^1.0.17:
formidable@^1.0.17, formidable@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9"
@@ -4013,6 +4024,10 @@ hoek@4.x.x:
version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
hoek@5.x.x:
version "5.0.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.3.tgz#b71d40d943d0a95da01956b547f83c4a5b4a34ac"
hoist-non-react-statics@^1.0.0, hoist-non-react-statics@^1.0.3, hoist-non-react-statics@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
@@ -4608,9 +4623,11 @@ isemail@1.x.x:
version "1.2.0"
resolved "https://registry.yarnpkg.com/isemail/-/isemail-1.2.0.tgz#be03df8cc3e29de4d2c5df6501263f1fa4595e9a"
isemail@2.x.x:
version "2.2.1"
resolved "https://registry.yarnpkg.com/isemail/-/isemail-2.2.1.tgz#0353d3d9a62951080c262c2aa0a42b8ea8e9e2a6"
isemail@3.x.x:
version "3.1.1"
resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.1.1.tgz#e8450fe78ff1b48347db599122adcd0668bd92b5"
dependencies:
punycode "2.x.x"
isexe@^2.0.0:
version "2.0.0"
@@ -4696,10 +4713,6 @@ istanbul-reports@^1.1.2:
dependencies:
handlebars "^4.0.3"
items@2.x.x:
version "2.1.1"
resolved "https://registry.yarnpkg.com/items/-/items-2.1.1.tgz#8bd16d9c83b19529de5aea321acaada78364a198"
iterall@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.0.2.tgz#41a2e96ce9eda5e61c767ee5dc312373bb046e91"
@@ -4933,14 +4946,13 @@ jest@^21.2.1:
dependencies:
jest-cli "^21.2.1"
joi@^10.6.0:
version "10.6.0"
resolved "https://registry.yarnpkg.com/joi/-/joi-10.6.0.tgz#52587f02d52b8b75cdb0c74f0b164a191a0e1fc2"
joi@^13.0.0:
version "13.1.2"
resolved "https://registry.yarnpkg.com/joi/-/joi-13.1.2.tgz#b2db260323cc7f919fafa51e09e2275bd089a97e"
dependencies:
hoek "4.x.x"
isemail "2.x.x"
items "2.x.x"
topo "2.x.x"
hoek "5.x.x"
isemail "3.x.x"
topo "3.x.x"
joi@^6.10.1:
version "6.10.1"
@@ -5109,7 +5121,7 @@ jsonpointer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
jsonwebtoken@^7.0.0, jsonwebtoken@^7.4.3:
jsonwebtoken@^7.0.0:
version "7.4.3"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.4.3.tgz#77f5021de058b605a1783fa1283e99812e645638"
dependencies:
@@ -5119,6 +5131,21 @@ jsonwebtoken@^7.0.0, jsonwebtoken@^7.4.3:
ms "^2.0.0"
xtend "^4.0.1"
jsonwebtoken@^8.0.0:
version "8.1.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.1.1.tgz#b04d8bb2ad847bc93238c3c92170ffdbdd1cb2ea"
dependencies:
jws "^3.1.4"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
xtend "^4.0.1"
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -5463,6 +5490,10 @@ lodash.get@4.4.2, lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
lodash.isarguments@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
@@ -5471,6 +5502,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.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
lodash.isempty@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
@@ -5479,11 +5514,19 @@ lodash.isequal@^4.4.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
lodash.isobject@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d"
lodash.isplainobject@^4.0.0:
lodash.isplainobject@^4.0.0, lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
@@ -5977,7 +6020,7 @@ ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
ms@^2.0.0:
ms@^2.0.0, ms@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
@@ -6366,6 +6409,10 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
oauth@0.9.x:
version "0.9.15"
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -6633,6 +6680,18 @@ parseurl@~1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
passport-facebook@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/passport-facebook/-/passport-facebook-2.1.1.tgz#c39d0b52ae4d59163245a4e21a7b9b6321303311"
dependencies:
passport-oauth2 "1.x.x"
passport-google-oauth2@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/passport-google-oauth2/-/passport-google-oauth2-0.1.6.tgz#dfd7016ac7449fe27cfeb252ae974afc23257a0d"
dependencies:
passport-oauth2 "^1.1.2"
passport-jwt@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-3.0.1.tgz#e4f7276dad8bd251d43c6fc38883130b963272f6"
@@ -6646,6 +6705,15 @@ passport-local@^1.0.0:
dependencies:
passport-strategy "1.x.x"
passport-oauth2@1.x.x, passport-oauth2@^1.1.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.4.0.tgz#f62f81583cbe12609be7ce6f160b9395a27b86ad"
dependencies:
oauth "0.9.x"
passport-strategy "1.x.x"
uid2 "0.0.x"
utils-merge "1.x.x"
passport-strategy@1.x.x, passport-strategy@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
@@ -7507,6 +7575,10 @@ punycode@1.4.1, punycode@^1.2.4, punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
punycode@2.x.x:
version "2.1.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
pym.js@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/pym.js/-/pym.js-1.3.2.tgz#0ebd083c5a7ef7650214db872b4b29a10743305d"
@@ -7519,7 +7591,7 @@ q@^1.1.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
qs@6.5.1, qs@^6.1.0, qs@^6.2.0, qs@~6.5.1:
qs@6.5.1, qs@^6.1.0, qs@^6.2.0, qs@^6.5.1, qs@~6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
@@ -7663,6 +7735,14 @@ react-input-autosize@^1.1.4:
create-react-class "^15.5.2"
prop-types "^15.5.8"
react-linkify@^0.2.1:
version "0.2.2"
resolved "https://registry.yarnpkg.com/react-linkify/-/react-linkify-0.2.2.tgz#55b99b1cc7244446a0f9bdebbe13b2c30f789e65"
dependencies:
linkify-it "^2.0.3"
prop-types "^15.5.8"
tlds "^1.57.0"
react-mdl-selectfield@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/react-mdl-selectfield/-/react-mdl-selectfield-0.2.0.tgz#36e1a97233036c057ab2bdb31ec09ad8d9988411"
@@ -8844,6 +8924,21 @@ superagent@^2.0.0:
qs "^6.1.0"
readable-stream "^2.0.5"
superagent@^3.8.0:
version "3.8.2"
resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403"
dependencies:
component-emitter "^1.2.0"
cookiejar "^2.1.0"
debug "^3.1.0"
extend "^3.0.0"
form-data "^2.3.1"
formidable "^1.1.1"
methods "^1.1.1"
mime "^1.4.1"
qs "^6.5.1"
readable-stream "^2.0.5"
supports-color@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"
@@ -9033,7 +9128,7 @@ title-case-minors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/title-case-minors/-/title-case-minors-1.0.0.tgz#51f17037c294747a1d1cda424b5004c86d8eb115"
tlds@^1.196.0:
tlds@^1.196.0, tlds@^1.57.0:
version "1.199.0"
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.199.0.tgz#a4fc8c3058216488a80aaaebb427925007e55217"
@@ -9100,11 +9195,11 @@ topo@1.x.x:
dependencies:
hoek "2.x.x"
topo@2.x.x:
version "2.0.2"
resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182"
topo@3.x.x:
version "3.0.0"
resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.0.tgz#37e48c330efeac784538e0acd3e62ca5e231fe7a"
dependencies:
hoek "4.x.x"
hoek "5.x.x"
touch@^3.1.0:
version "3.1.0"
@@ -9234,6 +9329,10 @@ uid-number@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
uid2@0.0.x:
version "0.0.3"
resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82"
ultron@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864"
@@ -9363,7 +9462,7 @@ util@0.10.3, "util@>=0.10.3 <1", util@^0.10.3:
dependencies:
inherits "2.0.1"
utils-merge@1.0.1:
utils-merge@1.0.1, utils-merge@1.x.x:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"