mirror of
https://github.com/wassname/talk.git
synced 2026-06-28 21:13:12 +08:00
Merge branch 'master' into docs
This commit is contained in:
+2
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
line-height: 22px;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -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
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 帳號"
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 = {};
|
||||
@@ -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
@@ -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
@@ -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
@@ -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, {});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user