Merge branch 'master' of github.com:coralproject/talk into organization-contact

* 'master' of github.com:coralproject/talk: (36 commits)
  Fixes #1505
  PropTypes
  Removin actions, using routes, and continuing the transition when the user makes an action
  fixes related to external auth
  Adding size, and proptypes
  IfSlotIsNotEmpty Bug
  Style and translations
  allow plugin code from clients to be parsed by webpack
  SaveChangesDialog
  Spanish translations
  Adding translations
  alert route hook ready!
  Removing plugin
  Restore Links
  Removing plugin
  Save changes / discard changes dialohg
  router and state lifted
  routes
  renaming slot :D
  renaming
  ...
This commit is contained in:
okbel
2018-04-09 16:51:06 -03:00
41 changed files with 733 additions and 233 deletions
+1
View File
@@ -6,6 +6,7 @@ npm-debug.log*
dump.rdb
client/coral-framework/graphql/introspection.json
docs/source/_data/introspection.json
.env
*.cfg
+19 -13
View File
@@ -13,6 +13,7 @@ const CommentModel = require('../models/comment');
const AssetsService = require('../services/assets');
const mongoose = require('../services/mongoose');
const scraper = require('../services/scraper');
const Context = require('../graph/context');
const inquirer = require('inquirer');
const { URL } = require('url');
@@ -52,22 +53,27 @@ async function refreshAssets(ageString) {
const ageMs = parseDuration(ageString);
const age = new Date(now - ageMs);
let assets = await AssetModel.find({
$or: [
{
scraped: {
$lte: age,
let assets = await AssetModel.find(
{
$or: [
{
scraped: {
$lte: age,
},
},
},
{
scraped: null,
},
],
});
{
scraped: null,
},
],
},
{ id: 1 }
);
// Create a graph context.
const ctx = Context.forSystem();
// Queue all the assets for scraping.
await Promise.all(assets.map(scraper.create));
await Promise.all(assets.map(({ id }) => scraper.create(ctx, id)));
console.log('Assets were queued to be scraped');
util.shutdown();
} catch (e) {
+14 -2
View File
@@ -2,10 +2,15 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
import Configure from 'routes/Configure';
import Install from 'routes/Install';
import Stories from 'routes/Stories';
import Community from 'routes/Community';
import Configure from 'routes/Configure';
import StreamSettings from './routes/Configure/containers/StreamSettings';
import ModerationSettings from './routes/Configure/containers/ModerationSettings';
import TechSettings from './routes/Configure/containers/TechSettings';
import { ModerationLayout, Moderation } from 'routes/Moderation';
import Layout from 'containers/Layout';
@@ -15,7 +20,14 @@ const routes = (
<Route exact path="/admin/install" component={Install} />
<Route path="/admin" component={Layout}>
<IndexRedirect to="/admin/moderate" />
<Route path="configure" component={Configure} />
<Route path="configure" component={Configure}>
<Route path="stream" component={StreamSettings} />
<Route path="moderation" component={ModerationSettings} />
<Route path="tech" component={TechSettings} />
<IndexRedirect to="stream" />
</Route>
<Route path="stories" component={Stories} />
{/* Community Routes */}
+6 -2
View File
@@ -8,6 +8,10 @@ export const clearPending = () => {
return { type: actions.CLEAR_PENDING };
};
export const setActiveSection = section => {
return { type: actions.SET_ACTIVE_SECTION, section };
export const showSaveDialog = () => {
return { type: actions.SHOW_SAVE_DIALOG };
};
export const hideSaveDialog = () => {
return { type: actions.HIDE_SAVE_DIALOG };
};
@@ -2,4 +2,6 @@ const prefix = 'TALK_ADMIN_CONFIGURE';
export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`;
export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`;
export const SET_ACTIVE_SECTION = `${prefix}_SET_ACTIVE_SECTION`;
export const SHOW_SAVE_DIALOG = `${prefix}_SHOW_SAVE_DIALOG`;
export const HIDE_SAVE_DIALOG = `${prefix}_HIDE_SAVE_DIALOG`;
+15 -6
View File
@@ -6,11 +6,23 @@ const initialState = {
canSave: false,
pending: {},
errors: {},
activeSection: 'stream',
saveDialog: false,
};
export default function configure(state = initialState, action) {
switch (action.type) {
case actions.SHOW_SAVE_DIALOG: {
return {
...state,
saveDialog: true,
};
}
case actions.HIDE_SAVE_DIALOG: {
return {
...state,
saveDialog: false,
};
}
case actions.UPDATE_PENDING: {
let next = state;
if (action.updater) {
@@ -40,11 +52,8 @@ export default function configure(state = initialState, action) {
pending: {},
canSave: false,
};
case actions.SET_ACTIVE_SECTION:
return {
...state,
activeSection: action.section,
};
default:
return state;
}
return state;
}
@@ -1,53 +1,37 @@
import React from 'react';
import t from 'coral-framework/services/i18n';
import { can } from 'coral-framework/services/perms';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import { Button, List, Item } from 'coral-ui';
import StreamSettings from '../containers/StreamSettings';
import ModerationSettings from '../containers/ModerationSettings';
import TechSettings from '../containers/TechSettings';
import OrganizationSettings from '../containers/OrganizationSettings';
import { can } from 'coral-framework/services/perms';
import styles from './Configure.css';
import SaveChangesDialog from './SaveChangesDialog';
class Configure extends React.Component {
getSectionComponent(section) {
switch (section) {
case 'stream':
return StreamSettings;
case 'moderation':
return ModerationSettings;
case 'tech':
return TechSettings;
case 'organization':
return OrganizationSettings;
default:
return StreamSettings;
}
}
render() {
const {
currentUser,
canSave,
savePending,
setActiveSection,
activeSection,
} = this.props;
const SectionComponent = this.getSectionComponent(activeSection);
const { canSave, currentUser, root, savePending, settings } = this.props;
if (!can(currentUser, 'UPDATE_CONFIG')) {
return (
<p>
You must be an administrator to access config settings. Please find
the nearest Admin and ask them to level you up!
</p>
);
return <p>{t('configure.access_message')}</p>;
}
const passProps = {
root,
settings,
};
return (
<div className={styles.container}>
<SaveChangesDialog
saveDialog={this.props.saveDialog}
hideSaveDialog={this.props.hideSaveDialog}
saveChanges={this.props.saveChanges}
discardChanges={this.props.discardChanges}
/>
<div className={styles.leftColumn}>
<List onChange={setActiveSection} activeItem={activeSection}>
<List
onChange={this.props.handleSectionChange}
activeItem={this.props.activeSection}
>
<Item itemId="stream" icon="speaker_notes">
{t('configure.stream_settings')}
</Item>
@@ -80,10 +64,7 @@ class Configure extends React.Component {
</div>
</div>
<div className={styles.mainContent}>
<SectionComponent
root={this.props.root}
settings={this.props.settings}
/>
{React.cloneElement(this.props.children, passProps)}
</div>
</div>
);
@@ -92,12 +73,17 @@ class Configure extends React.Component {
Configure.propTypes = {
savePending: PropTypes.func.isRequired,
saveChanges: PropTypes.func.isRequired,
discardChanges: PropTypes.func.isRequired,
currentUser: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
setActiveSection: PropTypes.func.isRequired,
handleSectionChange: PropTypes.func.isRequired,
activeSection: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
saveDialog: PropTypes.bool,
hideSaveDialog: PropTypes.func.isRequired,
};
export default Configure;
@@ -17,7 +17,9 @@ class OrganizationSettings extends React.Component {
const { settings, slotPassthrough } = this.props;
return (
<ConfigurePage title={t('configure.organization_settings')}>
<ConfigurePage title={t('configure.organization_information')}>
<p>{t('configure.organization_info_copy')}</p>
<p>{t('configure.organization_info_copy2')}</p>
<ConfigureCard title={t('configure.organization_details')}>
<ul>
<li>
@@ -0,0 +1,40 @@
.buttonActions {
padding-top: 15px;
text-align: right;
}
.dialog {
padding: 25px;
min-width: 400px;
}
.close {
font-size: 20px;
line-height: 14px;
top: 10px;
right: 10px;
position: absolute;
display: block;
font-weight: bold;
color: #363636;
cursor: pointer;
}
.title {
font-size: 18px;
font-weight: 800;
margin-bottom: 20px;
}
.cancel {
color: #363636;
margin-right: 15px;
display: inline-block;
&:hover {
cursor: pointer;
}
}
.button {
margin-left: 5px;
}
@@ -0,0 +1,53 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import { Button, Dialog } from 'coral-ui';
import styles from './SaveChangesDialog.css';
import t from 'coral-framework/services/i18n';
const SaveChangesDialog = ({
saveDialog,
hideSaveDialog,
saveChanges,
discardChanges,
}) => (
<Dialog
className={cn(styles.dialog, 'talk-admin-configure-save-dialog')}
id="saveDialog"
open={saveDialog}
onCancel={hideSaveDialog}
>
<span className={styles.close} onClick={hideSaveDialog}>
×
</span>
<div className={styles.title}>
{t('configure.save_changes_dialog.unsaved_changes')}
</div>
{t('configure.save_changes_dialog.copy')}
<div
className={cn(
styles.buttonActions,
'talk-admin-configure-save-dialog-button-actions'
)}
>
<a className={styles.cancel} onClick={hideSaveDialog}>
Cancel
</a>
<Button onClick={discardChanges} className={styles.button}>
{t('configure.save_changes_dialog.discard')}
</Button>
<Button onClick={saveChanges} cStyle="green" className={styles.button}>
{t('configure.save_changes_dialog.save_settings')}
</Button>
</div>
</Dialog>
);
SaveChangesDialog.propTypes = {
saveDialog: PropTypes.bool.isRequired,
hideSaveDialog: PropTypes.func.isRequired,
saveChanges: PropTypes.func.isRequired,
discardChanges: PropTypes.func.isRequired,
};
export default SaveChangesDialog;
@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
@@ -10,16 +10,71 @@ import { getDefinitionName } from 'coral-framework/utils';
import StreamSettings from './StreamSettings';
import TechSettings from './TechSettings';
import ModerationSettings from './ModerationSettings';
import { clearPending, setActiveSection } from '../../../actions/configure';
import {
clearPending,
showSaveDialog,
hideSaveDialog,
} from '../../../actions/configure';
import Configure from '../components/Configure';
import OrganizationSettings from './OrganizationSettings';
import { withRouter } from 'react-router';
class ConfigureContainer extends React.Component {
state = { nextRoute: '' };
class ConfigureContainer extends Component {
savePending = async () => {
await this.props.updateSettings(this.props.pending);
this.props.clearPending();
};
saveChanges = async () => {
await this.savePending();
this.props.hideSaveDialog();
this.gotoNextRoute();
};
discardChanges = async () => {
await this.props.clearPending();
this.props.hideSaveDialog();
this.gotoNextRoute();
};
gotoNextRoute = () => {
const { nextRoute } = this.state;
if (nextRoute) {
this.props.router.push(nextRoute);
this.setState({ nextRoute: '' });
}
};
handleSectionChange = async section => {
const nextRoute = `/admin/configure/${section}`;
if (this.shouldShowSaveDialog()) {
await this.setState({ nextRoute });
this.props.showSaveDialog();
} else {
// Just go to the section
this.props.router.push(nextRoute);
}
};
shouldShowSaveDialog = () => {
return !!Object.keys(this.props.pending).length;
};
routeLeave = ({ pathname }) => {
if (this.shouldShowSaveDialog()) {
this.setState({ nextRoute: pathname });
this.props.showSaveDialog();
return false;
}
};
componentDidMount() {
this.props.router.setRouteLeaveHook(this.props.route, this.routeLeave);
}
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
@@ -31,14 +86,20 @@ class ConfigureContainer extends Component {
return (
<Configure
saveChanges={this.saveChanges}
discardChanges={this.discardChanges}
saveDialog={this.props.saveDialog}
activeSection={this.props.routes[3].path}
hideSaveDialog={this.props.hideSaveDialog}
canSave={this.props.canSave}
currentUser={this.props.currentUser}
root={this.props.root}
settings={this.props.mergedSettings}
canSave={this.props.canSave}
handleSectionChange={this.handleSectionChange}
savePending={this.savePending}
setActiveSection={this.props.setActiveSection}
activeSection={this.props.activeSection}
/>
>
{this.props.children}
</Configure>
);
}
}
@@ -79,18 +140,21 @@ const mapStateToProps = state => ({
pending: state.configure.pending,
canSave: state.configure.canSave,
activeSection: state.configure.activeSection,
saveDialog: state.configure.saveDialog,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
clearPending,
setActiveSection,
showSaveDialog,
hideSaveDialog,
},
dispatch
);
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps),
withUpdateSettings,
withConfigureQuery,
@@ -98,14 +162,20 @@ export default compose(
)(ConfigureContainer);
ConfigureContainer.propTypes = {
activeSection: PropTypes.string,
updateSettings: PropTypes.func.isRequired,
clearPending: PropTypes.func.isRequired,
setActiveSection: PropTypes.func.isRequired,
showSaveDialog: PropTypes.func.isRequired,
hideSaveDialog: PropTypes.func.isRequired,
saveDialog: PropTypes.bool.isRequired,
currentUser: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
pending: PropTypes.object.isRequired,
mergedSettings: PropTypes.object.isRequired,
activeSection: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
router: PropTypes.object,
route: PropTypes.object,
routes: PropTypes.array,
};
@@ -10,12 +10,14 @@ const InitialStep = () => {
return (
<div className={cn(styles.step, styles.finalStep, 'talk-install-step-5')}>
<p>{t('install.final.description')}</p>
<Button raised>
<Link to="/admin">{t('install.final.launch')}</Link>
</Button>
<Button cStyle="black" raised>
<a href="http://coralproject.net">{t('install.final.close')}</a>
</Button>
<Link to="/admin">
<Button raised>{t('install.final.launch')}</Button>
</Link>
<a href="http://coralproject.net">
<Button cStyle="black" raised>
{t('install.final.close')}
</Button>
</a>
</div>
);
};
@@ -35,12 +35,9 @@ class ExtendableTabPanelContainer extends React.Component {
createPluginTabFactory = (props = this.props) => el => {
return (
<ExtendableTab
tabId={el.type.talkPluginName}
key={el.type.talkPluginName}
>
<ExtendableTab tabId={el.key} key={el.key}>
{React.cloneElement(el, {
active: props.activeTab === el.type.talkPluginName,
active: props.activeTab === el.key,
})}
</ExtendableTab>
);
@@ -59,7 +56,7 @@ class ExtendableTabPanelContainer extends React.Component {
createPluginTabPane(el) {
return (
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
<TabPane tabId={el.key} key={el.key}>
{el}
</TabPane>
);
@@ -34,6 +34,7 @@ class Comment extends React.Component {
defaultComponent={CommentContent}
className={cn(styles.commentBody, 'my-comment-body')}
passthrough={slotPassthrough}
size={1}
/>
<div className={cn(styles.commentSummary, 'comment-summary')}>
<span
+5
View File
@@ -57,6 +57,10 @@ export const setAuthToken = token => (dispatch, _, { localStorage }) => {
localStorage.setItem('token', token);
}
// Dispatch the set auth token action. For some browsers and situations, we
// may not be able to persist the auth token any other way. Keep it in redux!
dispatch({ type: actions.SET_AUTH_TOKEN, token });
dispatch(checkLogin());
};
@@ -87,6 +91,7 @@ export const handleSuccessfulLogin = (user, token) => (
dispatch({
type: actions.HANDLE_SUCCESSFUL_LOGIN,
user,
token,
});
};
@@ -7,7 +7,7 @@ class IfSlotIsNotEmpty extends React.Component {
isSlotEmpty(props = this.props) {
const { slotElements } = props;
return slotElements.length === 0
? false
? true
: slotElements.every(elements => elements.length === 0);
}
@@ -19,6 +19,7 @@ class IfSlotIsNotEmpty extends React.Component {
IfSlotIsNotEmpty.propTypes = {
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
slotElements: PropTypes.array.isRequired,
children: PropTypes.node.isRequired,
passthrough: PropTypes.object.isRequired,
};
+2
View File
@@ -1,5 +1,7 @@
const prefix = `TALK_FRAMEWORK`;
export const SET_AUTH_TOKEN = `${prefix}_SET_AUTH_TOKEN`;
export const CHECK_LOGIN_REQUEST = `${prefix}_CHECK_LOGIN_REQUEST`;
export const CHECK_LOGIN_SUCCESS = `${prefix}_CHECK_LOGIN_SUCCESS`;
export const CHECK_LOGIN_FAILURE = `${prefix}_CHECK_LOGIN_FAILURE`;
+9
View File
@@ -5,6 +5,7 @@ const initialState = {
checkedInitialLogin: false,
initialLoginError: null,
user: null,
token: null,
};
const purge = user => {
@@ -14,12 +15,18 @@ const purge = user => {
export default function auth(state = initialState, action) {
switch (action.type) {
case actions.SET_AUTH_TOKEN:
return {
...state,
token: action.token || null,
};
case actions.CHECK_LOGIN_FAILURE:
return {
...state,
initialLoginError: action.error,
checkedInitialLogin: true,
user: null,
token: null,
};
case actions.CHECK_LOGIN_SUCCESS:
return {
@@ -31,11 +38,13 @@ export default function auth(state = initialState, action) {
return {
...state,
user: action.user ? purge(action.user) : null,
token: action.token || null,
};
case actions.LOGOUT:
return {
...state,
user: null,
token: null,
};
case actions.UPDATE_STATUS: {
return {
+7 -1
View File
@@ -47,6 +47,12 @@ const getAuthToken = (store, storage) => {
} else if (!bowser.safari && !bowser.ios && storage) {
// Use local storage auth tokens where there's a stable api.
return storage.getItem('token');
} else if (state.auth && state.auth.token) {
// Use the redux token state if the remaining methods fall out. If the embed
// is called with `embed.login(token)`, and the browser is not capable of
// storing the token in localStorage, then we would have persisted it to the
// redux state.
return state.auth.token;
}
return null;
@@ -123,7 +129,7 @@ export async function createContext({
// Try to get the token from localStorage. If it isn't here, it may
// be passed as a cookie.
// NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT
// NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERENT
// TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT.
return getAuthToken(store, localStorage);
};
+1
View File
@@ -136,6 +136,7 @@ export function t(key, ...replacements) {
replacements.forEach((str, i) => {
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
});
return translation;
} else {
console.warn(`${lang}.${key} and en.${key} language key not set`);
+21 -2
View File
@@ -173,11 +173,30 @@ class PluginsService {
);
}
/**
* This adds a consistent keying for the slot elements.
* It uses the plugin name as the key. If the same plugin inserts
* multiple elements it will append `.${noOfOccurence}` to the
* key starting with the second element.
*/
const getKey = (() => {
const map = {};
return component => {
if (map[component.talkPluginName] === undefined) {
map[component.talkPluginName] = 0;
} else {
map[component.talkPluginName]++;
}
const i = map[component.talkPluginName];
return `${component.talkPluginName}${i > 0 ? `.${i}` : ''}`;
};
})();
return (size > 0 ? slots.slice(0, size) : slots)
.map((component, i) => ({
.map(component => ({
component,
disabled: isDisabled(component),
key: i,
key: getKey(component),
}))
.filter(o => !o.disabled)
.map(({ component, key }) =>
+4
View File
@@ -59,6 +59,10 @@ const CONFIG = {
// it was built at.
REVISION_HASH: process.env.REVISION_HASH,
// SCRAPER_HEADERS is a JSON string that will be used to override the headers
// on the scraper when it makes requests.
SCRAPER_HEADERS: process.env.TALK_SCRAPER_HEADERS || '{}',
//------------------------------------------------------------------------------
// JWT based configuration
//------------------------------------------------------------------------------
+1 -5
View File
@@ -167,14 +167,10 @@ TALK_REDIS_URL=redis://127.0.0.1:6379
TALK_ROOT_URL=http://127.0.0.1:3000
TALK_PORT=3000
TALK_JWT_SECRET=password
TALK_FACEBOOK_APP_ID=A-Facebook-App-ID
TALK_FACEBOOK_APP_SECRET=A-Facebook-App-Secret
```
This is only the bare minimum needed to run the demo, for more configuration
variables, check out the [Configuration](/talk/configuration/) section. Facebook login above
will definitely not work unless you change those values as well.
variables, check out the [Configuration](/talk/configuration/) section.
You can now start the application by running:
@@ -57,14 +57,11 @@ TALK_REDIS_URL=redis://127.0.0.1:6379
TALK_ROOT_URL=http://127.0.0.1:3000
TALK_PORT=3000
TALK_JWT_SECRET=password
TALK_FACEBOOK_APP_ID=A-Facebook-App-ID
TALK_FACEBOOK_APP_SECRET=A-Facebook-App-Secret
```
This is the bare minimum needed to start Talk, for more configuration
variables, check out the [Configuration](/talk/configuration/)
section. Facebook login above will definitely not work unless you change those
values as well.
section.
You can now start the application by running:
+8 -1
View File
@@ -524,4 +524,11 @@ Sets the logging level for the context logger (from [Bunyan](https://github.com/
- `warn`
- `info`
- `debug`
- `trace`
- `trace`
## TALK_SCRAPER_HEADERS
A JSON string representing the configuration passed to the
[fetch](https://www.npmjs.com/package/node-fetch) call for the scraper. It
can be used to set an authorization header, or change the user agent. (Default
`{}`)
@@ -24,7 +24,7 @@ All levels of comments and replies are able to be linked to via permalink. Perma
```text
https://<your asset url>?commentId=<the comment id>
```
{:.no-copy}
### Threading
@@ -44,7 +44,7 @@ talk-stream-comment-level-${depth}
talk-stream-highlighted-comment
talk-stream-pending-comment
```
{:.no-copy}
### Automatic Updates
+48 -13
View File
@@ -1,21 +1,56 @@
const Asset = require('../models/asset');
const scraper = require('../services/scraper');
const Assets = require('../services/assets');
const { createLogger } = require('../services/logging');
const Asset = require('../../models/asset');
const scraper = require('../../services/scraper');
const Assets = require('../../services/assets');
const { createLogger } = require('../../services/logging');
const logger = createLogger('jobs:scraper');
const metascraper = require('metascraper');
const fetch = require('node-fetch');
const { merge } = require('lodash');
const { version } = require('../../package.json');
const { SCRAPER_HEADERS } = require('../../config');
// Load the scraper with the rules.
const metascraper = require('metascraper').load([
require('metascraper-title')(),
require('metascraper-description')(),
require('metascraper-image')(),
require('metascraper-author')(),
require('metascraper-date')(),
require('./rules/modified')(),
require('./rules/section')(),
]);
let customHeaders = {};
try {
customHeaders = JSON.parse(SCRAPER_HEADERS);
} catch (err) {
console.error('Cannot parse TALK_SCRAPER_HEADERS');
throw err;
}
// Parse the headers to be added to the scraper.
const headers = merge(
{
'User-Agent': `Coral-Talk/${version}`,
},
customHeaders
);
/**
* Scrapes the given asset for metadata.
*/
async function scrape(asset) {
return metascraper.scrapeUrl(
asset.url,
Object.assign({}, metascraper.RULES, {
section: $ => $('meta[property="article:section"]').attr('content'),
modified: $ => $('meta[property="article:modified"]').attr('content'),
})
);
async function scrape({ url }) {
const res = await fetch(url, {
headers,
});
const html = await res.text();
// Get the metadata from the scraped html.
const metadata = await metascraper({
html,
url,
});
return metadata;
}
/**
+5
View File
@@ -0,0 +1,5 @@
module.exports = () => ({
modified: [
({ htmlDom: $ }) => $('meta[property="article:modified"]').attr('content'),
],
});
+5
View File
@@ -0,0 +1,5 @@
module.exports = () => ({
section: [
({ htmlDom: $ }) => $('meta[property="article:section"]').attr('content'),
],
});
+10 -1
View File
@@ -154,16 +154,25 @@ en:
sign_out: "Sign Out"
stories: Stories
stream_settings: "Stream Settings"
access_message: "You must be an administrator to access config settings. Please find the nearest Admin and ask them to level you up!"
suspect_word_title: "Suspect words list"
suspect_word_text: "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list."
tech_settings: "Tech Settings"
organization_settings: "Organization Settings"
organization_information: "Organization information"
organization_info_copy: "We use this information in email notifications generated by Talk. This connects the messages to your organization, and provides a way for users to contact you if they have an issue with their account."
organization_info_copy_2: "We recommend creating a generic email account (eg. community@yournewsroom.com) for this purpuse. This means it can remain consistent over time, and doesn't expose a name that users could target if their account were blocked."
organization_details: "Organization Details"
organization_name: "Organization Name"
organization_contact_email: "Organization Contact Email"
title: "Configure Comment Stream"
weeks: Weeks
wordlist: "Banned Words"
save_changes_dialog:
unsaved_changes: "Unsaved changes"
copy: "You have made one or more changes without saving. Would you like to save or discard your changes?"
save_settings: "Save Settings"
discard: "Discard"
cancel: "Cancel"
continue: "Continue"
createdisplay:
check_the_form: "Invalid Form. Please check the fields"
+10 -1
View File
@@ -153,16 +153,25 @@ es:
sign_out: "Desconectar"
stories: Artículos
stream_settings: "Configuración de Comentarios"
access_message: "Usted debe ser un administrador para acceder a esta página. Encuentre a otro admin y actualice los permisos de su cuenta!"
suspect_word_title: "Lista de palabras sospechosas"
suspect_word_text: "Comentarios que contengan estas palabras o frases, considerando mayusculas y minúsculas, serán automáticamente destacadas en los comentarios publicados. Escribir una palabra y apretar Enter o Tabulador para agregarla. O pegar una lista de palabras separadas por coma."
tech_settings: "Configuración Técnica"
organization_settings: "Configuración de la Organización"
organization_information: "Información de la Organización"
organization_details: "Detalles de la Organización"
organization_info_copy: "Nosotros usamos esta información en las notificaciones de email generadas por Talk. Esto conecta los mensajes de tu organización, y provee una forma para que los usuarios se comuniquen si tienen un inconveniente con su cuenta."
organization_info_copy_2: "Recomendamos crear un email genérico (ej: community@yournewsroom.com) for this purpose. Esto significa que puede permanecer consistente con el tiempo y no expone un nombre que los usuarios puedan atacar si su cuenta fue bloqueada."
organization_name: "Nombre de la Organización"
organization_contact_email: "Email de la Organización"
title: "Configurar los comentarios"
weeks: Semanas
wordlist: "Palabras Suspendidas"
save_changes_dialog:
unsaved_changes: "Cambios no guardados"
copy: Has hecho uno o más cambios sin guardar. Deseas guardar o descartar tus cambios?"
save_settings: "Guardar configuración"
discard: "Descartar"
cancel: "Cancelar"
continue: "Continuar"
createdisplay:
check_the_form: "Formulario Inválido. Por favor verifica los campos"
+6 -1
View File
@@ -137,7 +137,12 @@
"lodash-es": "^4.16.6",
"marked": "^0.3.6",
"material-design-lite": "^1.2.1",
"metascraper": "1.0.7",
"metascraper": "^3.9.2",
"metascraper-author": "^3.9.2",
"metascraper-date": "^3.3.0",
"metascraper-description": "^3.9.2",
"metascraper-image": "^3.9.2",
"metascraper-title": "^3.9.2",
"minimist": "^1.2.0",
"moment": "^2.18.1",
"mongoose": "^4.12.3",
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Slot } from 'plugin-api/beta/client/components';
import {
Button,
TextField,
@@ -28,6 +29,15 @@ class SignUp extends React.Component {
this.props.onSubmit();
};
childFactory = el => {
const key = el.key;
const props = {
indicateBlocker: () => this.props.indicateBlocker(key),
indicateBlockerResolved: () => this.props.indicateBlockerResolved(key),
};
return React.cloneElement(el, props);
};
render() {
const {
username,
@@ -42,6 +52,7 @@ class SignUp extends React.Component {
errorMessage,
requireEmailConfirmation,
success,
blocked,
} = this.props;
return (
@@ -103,6 +114,10 @@ class SignUp extends React.Component {
onChange={this.handlePasswordRepeatChange}
minLength="8"
/>
<Slot
fill="talkPluginAuth.formField"
childFactory={this.childFactory}
/>
<div className={styles.action}>
<Button
type="submit"
@@ -110,6 +125,7 @@ class SignUp extends React.Component {
id="coralSignUpButton"
className={styles.button}
full
disabled={blocked}
>
{t('talk-plugin-auth.login.sign_up')}
</Button>
@@ -161,6 +177,9 @@ SignUp.propTypes = {
errorMessage: PropTypes.string,
requireEmailConfirmation: PropTypes.bool.isRequired,
success: PropTypes.bool.isRequired,
blocked: PropTypes.bool.isRequired,
indicateBlocker: PropTypes.func.isRequired,
indicateBlockerResolved: PropTypes.func.isRequired,
};
export default SignUp;
@@ -16,8 +16,19 @@ class SignUpContainer extends Component {
emailError: null,
passwordError: null,
passwordRepeatError: null,
blockers: [],
};
indicateBlocker = key =>
this.setState(state => ({
blockers: state.blockers.concat(key),
}));
indicateBlockerResolved = key =>
this.setState(state => ({
blockers: state.blockers.filter(i => i !== key),
}));
validate = data => {
let valid = true;
const changes = {};
@@ -48,7 +59,7 @@ class SignUpContainer extends Component {
passwordRepeat: this.state.passwordRepeat,
};
if (this.validate(data)) {
if (this.validate(data) && !this.state.blockers.length) {
this.props.signUp(data);
}
};
@@ -76,6 +87,9 @@ class SignUpContainer extends Component {
render() {
return (
<SignUp
indicateBlocker={this.indicateBlocker}
indicateBlockerResolved={this.indicateBlockerResolved}
blocked={!!this.state.blockers.length}
onSubmit={this.handleSubmit}
onUsernameChange={this.setUsername}
onEmailChange={this.props.setEmail}
@@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './Comment.css';
import { t } from 'plugin-api/beta/client/services';
@@ -28,6 +29,7 @@ class Comment extends React.Component {
fill="commentContent"
defaultComponent={CommentContent}
passthrough={slotPassthrough}
size={1}
/>
<div className={cn(`${pluginName}-comment-username-box`)}>
@@ -87,4 +89,11 @@ class Comment extends React.Component {
}
}
Comment.propTypes = {
viewComment: PropTypes.func,
comment: PropTypes.object,
asset: PropTypes.object,
root: PropTypes.object,
};
export default Comment;
@@ -14,10 +14,10 @@ import cn from 'classnames';
class Settings extends React.Component {
childFactory = el => {
const pluginName = el.type.talkPluginName;
const key = el.key;
const props = {
indicateOn: () => this.props.indicateOn(pluginName),
indicateOff: () => this.props.indicateOff(pluginName),
indicateOn: () => this.props.indicateOn(key),
indicateOff: () => this.props.indicateOff(key),
};
return React.cloneElement(el, props);
};
@@ -20,14 +20,14 @@ class SettingsContainer extends React.Component {
turnOffInput: {},
};
indicateOn = plugin =>
indicateOn = key =>
this.setState(state => ({
hasNotifications: state.hasNotifications.concat(plugin),
hasNotifications: state.hasNotifications.concat(key),
}));
indicateOff = plugin =>
indicateOff = key =>
this.setState(state => ({
hasNotifications: state.hasNotifications.filter(i => i !== plugin),
hasNotifications: state.hasNotifications.filter(i => i !== key),
}));
setTurnOffInputFragment = fragment =>
@@ -1,12 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import Linkify from 'react-linkify';
import styles from './AdminCommentContent.css';
import { AdminCommentContent as Content } from 'plugin-api/beta/client/components';
class AdminCommentContent extends React.Component {
render() {
const { comment, suspectWords, bannedWords } = this.props;
return (
const content = (
<Content
className={styles.content}
body={comment.richTextBody ? comment.richTextBody : comment.body}
@@ -15,6 +16,12 @@ class AdminCommentContent extends React.Component {
html={!!comment.richTextBody}
/>
);
if (!!comment.richTextBody) {
return content;
}
return <Linkify properties={{ target: '_blank' }}>{content}</Linkify>;
}
}
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { PLUGIN_NAME } from '../constants';
import cn from 'classnames';
import styles from './CommentContent.css';
import Linkify from 'react-linkify';
class CommentContent extends React.Component {
render() {
@@ -14,7 +15,9 @@ class CommentContent extends React.Component {
dangerouslySetInnerHTML={{ __html: comment.richTextBody }}
/>
) : (
<div className={className}>{comment.body}</div>
<Linkify properties={{ target: '_blank' }}>
<div className={className}>{comment.body}</div>
</Linkify>
);
}
}
+18 -4
View File
@@ -22,6 +22,7 @@ require('dotenv').config();
const { plugins, pluginsPath, PluginManager } = require('./plugins');
const manager = new PluginManager(plugins);
const targetPlugins = manager.section('targets').plugins;
const clientPlugins = manager.section('client').plugins;
debug(`Using ${pluginsPath} as the plugin configuration path`);
@@ -51,6 +52,19 @@ const TALK_WEBPACK_SOURCE_MAP = _.get(
const devtool =
TALK_WEBPACK_SOURCE_MAP === 'none' ? false : TALK_WEBPACK_SOURCE_MAP;
// Exclude everything from node_modules except for those that may be in a
// plugin folder inside the node_modules folder.
const exclude = {
and: [
// Exclude everything inside node_modules..
/node_modules/,
// But not those that contain plugins.
{
not: clientPlugins.map(({ name }) => new RegExp(`node_modules\/${name}`)),
},
],
};
//==============================================================================
// Base Webpack Config
//==============================================================================
@@ -73,7 +87,7 @@ const config = {
},
{
loader: 'babel-loader',
exclude: /node_modules/,
exclude,
test: /\.js$/,
query: {
cacheDirectory: true,
@@ -82,11 +96,11 @@ const config = {
{
loader: 'hjson-loader',
test: /\.(json|yml)$/,
exclude: /node_modules/,
exclude,
},
{
loader: 'yaml-loader',
exclude: /node_modules/,
exclude,
test: /\.yml$/,
},
{
@@ -118,7 +132,7 @@ const config = {
},
{
test: /\.(graphql|gql)$/,
exclude: /node_modules/,
exclude,
loader: 'graphql-tag/loader',
},
],
+233 -99
View File
@@ -114,6 +114,18 @@
git-url-parse "^6.0.2"
shelljs "^0.7.0"
"@metascraper/helpers@^3.9.2":
version "3.9.2"
resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-3.9.2.tgz#c96588de5c6352d7ed9bf7245e35ac0c82333c59"
dependencies:
condense-whitespace "~1.0.0"
is-relative-url "~2.0.0"
lodash "~4.17.4"
normalize-url "~2.0.0"
smartquotes "~2.3.1"
to-title-case "~1.0.0"
url-regex "~4.1.1"
"@types/graphql@0.10.2":
version "0.10.2"
resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.10.2.tgz#d7c79acbaa17453b6681c80c34b38fcb10c4c08c"
@@ -315,6 +327,12 @@ ansi-styles@^3.1.0, ansi-styles@^3.2.0:
dependencies:
color-convert "^1.9.0"
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
dependencies:
color-convert "^1.9.0"
any-observable@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242"
@@ -323,10 +341,6 @@ any-promise@^0.1.0, any-promise@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-0.1.0.tgz#830b680aa7e56f33451d4b049f3bd8044498ee27"
any-promise@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
anymatch@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
@@ -517,7 +531,7 @@ array-union@^1.0.1:
dependencies:
array-uniq "^1.0.1"
array-uniq@^1.0.1:
array-uniq@^1.0.1, array-uniq@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
@@ -621,10 +635,6 @@ async@~0.2.6:
version "0.2.10"
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
async@~0.9.0:
version "0.9.2"
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
async@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9"
@@ -1826,6 +1836,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
chalk@^2.3.0, chalk@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65"
dependencies:
ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796"
@@ -1868,7 +1886,7 @@ cheerio@^0.20.0:
optionalDependencies:
jsdom "^7.0.2"
cheerio@^1.0.0-rc.2:
cheerio@^1.0.0-rc.2, cheerio@~1.0.0-rc.2:
version "1.0.0-rc.2"
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
dependencies:
@@ -1916,7 +1934,7 @@ chownr@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
chrono-node@^1.2.3:
chrono-node@~1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/chrono-node/-/chrono-node-1.3.5.tgz#a2495298a32da82bcc01ad9be7d77efa5e244122"
dependencies:
@@ -2127,12 +2145,6 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
dependencies:
delayed-stream "~1.0.0"
combined-stream@~0.0.4:
version "0.0.7"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-0.0.7.tgz#0137e657baa5a7541c57ac37ac5fc07d73b4dc1f"
dependencies:
delayed-stream "0.0.5"
command-exists@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.2.tgz#12819c64faf95446ec0ae07fe6cafb6eb3708b22"
@@ -2231,14 +2243,6 @@ concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
concat-stream@^1.4.7, concat-stream@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
dependencies:
inherits "^2.0.3"
readable-stream "^2.2.2"
typedarray "^0.0.6"
concat-stream@^1.5.0:
version "1.6.1"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.1.tgz#261b8f518301f1d834e36342b9fea095d2620a26"
@@ -2247,6 +2251,18 @@ concat-stream@^1.5.0:
readable-stream "^2.2.2"
typedarray "^0.0.6"
concat-stream@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
dependencies:
inherits "^2.0.3"
readable-stream "^2.2.2"
typedarray "^0.0.6"
condense-whitespace@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/condense-whitespace/-/condense-whitespace-1.0.0.tgz#8376d98ef028e6cb2cd2468e28ce42c5c65ab1a9"
configstore@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90"
@@ -2402,7 +2418,7 @@ cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
parse-json "^2.2.0"
require-from-string "^1.1.0"
cosmiconfig@^4.0.0:
cosmiconfig@^4.0.0, cosmiconfig@~4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-4.0.0.tgz#760391549580bbd2df1e562bc177b13c290972dc"
dependencies:
@@ -2805,10 +2821,6 @@ del@^2.0.2:
pinkie-promise "^2.0.0"
rimraf "^2.2.8"
delayed-stream@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-0.0.5.tgz#d4b1f43a93e8296dfe02694f4680bc37a313c73f"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -3876,14 +3888,6 @@ form-data@1.0.0-rc4:
combined-stream "^1.0.5"
mime-types "^2.1.10"
form-data@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-0.2.0.tgz#26f8bc26da6440e299cbdcfb69035c4f77a6e466"
dependencies:
async "~0.9.0"
combined-stream "~0.0.4"
mime-types "~2.0.3"
form-data@^2.3.1, form-data@~2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf"
@@ -4801,7 +4805,7 @@ html-entities@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
htmlparser2@^3.9.1:
htmlparser2@^3.9.0, htmlparser2@^3.9.1:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
dependencies:
@@ -5099,7 +5103,7 @@ ioredis@3.1.4, ioredis@^3.1.2:
redis-commands "^1.2.0"
redis-parser "^2.4.0"
ip-regex@^1.0.0:
ip-regex@^1.0.0, ip-regex@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd"
@@ -5294,10 +5298,6 @@ is-ip@1.0.0:
dependencies:
ip-regex "^1.0.0"
is-isodate@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/is-isodate/-/is-isodate-0.0.1.tgz#4fe2e937d50f3ba68c7b69b0218008b624aa5036"
is-my-json-valid@^2.12.4:
version "2.16.1"
resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11"
@@ -5399,6 +5399,12 @@ is-regexp@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
is-relative-url@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-relative-url/-/is-relative-url-2.0.0.tgz#72902d7fe04b3d4792e7db15f9db84b7204c9cef"
dependencies:
is-absolute-url "^2.0.0"
is-resolvable@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.1.tgz#acca1cd36dbe44b974b924321555a70ba03b1cf4"
@@ -5439,10 +5445,6 @@ is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
is-url@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.2.tgz#498905a593bf47cc2d9e7f738372bbf7696c7f26"
is-utf8@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
@@ -5490,6 +5492,10 @@ isomorphic-fetch@^2.1.1:
node-fetch "^1.0.1"
whatwg-fetch ">=0.10.0"
isostring@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isostring/-/isostring-0.0.1.tgz#ddb608efbfc89cda86db9cb16be090a788134c7f"
isstream@0.1.x, isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -6601,6 +6607,10 @@ lodash.endswith@^4.0.1, lodash.endswith@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09"
lodash.escaperegexp@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
lodash.flatten@^4.2.0, lodash.flatten@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
@@ -6856,16 +6866,6 @@ make-dir@^1.0.0:
dependencies:
pify "^3.0.0"
make-error-cause@^1.0.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d"
dependencies:
make-error "^1.2.0"
make-error@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.0.tgz#52ad3a339ccf10ce62b4040b708fe707244b8b96"
makeerror@1.0.x:
version "1.0.11"
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
@@ -6975,16 +6975,94 @@ merge@^1.1.3:
version "1.2.0"
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
metascraper@1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/metascraper/-/metascraper-1.0.7.tgz#2343f9108f34e4d2b55b7b4f543c96dfc52e1478"
metascraper-author@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/metascraper-author/-/metascraper-author-3.9.2.tgz#ff2020ac428f59a875d655df3b0d4bea171fde19"
dependencies:
cheerio "^0.20.0"
chrono-node "^1.2.3"
is-isodate "0.0.1"
is-url "^1.2.1"
popsicle "^6.2.0"
to-title-case "^1.0.0"
"@metascraper/helpers" "^3.9.2"
lodash "~4.17.4"
metascraper-date@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/metascraper-date/-/metascraper-date-3.3.0.tgz#359cc596896286ac06a677508fd66a0ee490fd17"
dependencies:
chrono-node "~1.3.5"
isostring "0.0.1"
metascraper-description@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-3.9.2.tgz#1afc983e7088f05e1a6961fe9772360aa0664bd1"
dependencies:
"@metascraper/helpers" "^3.9.2"
lodash "~4.17.4"
metascraper-image@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/metascraper-image/-/metascraper-image-3.9.2.tgz#029649356565b92c7685873c36a04f92ccb979e8"
dependencies:
"@metascraper/helpers" "^3.9.2"
metascraper-lang@^3.9.0:
version "3.9.0"
resolved "https://registry.yarnpkg.com/metascraper-lang/-/metascraper-lang-3.9.0.tgz#b66547c470618416dab89c8e221a29df9ac88f75"
dependencies:
lodash "~4.17.4"
metascraper-logo@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/metascraper-logo/-/metascraper-logo-3.9.2.tgz#9779c5efda3391a1642bd9b7e590855477644225"
dependencies:
"@metascraper/helpers" "^3.9.2"
lodash "~4.17.4"
metascraper-publisher@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/metascraper-publisher/-/metascraper-publisher-3.4.0.tgz#b1ecbe6b2300eca97e860ad322f817925dbfffe4"
dependencies:
condense-whitespace "~1.0.0"
lodash "~4.17.4"
metascraper-title@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/metascraper-title/-/metascraper-title-3.9.2.tgz#594bea98e2b364bf808d40720950f6a1e4057a1f"
dependencies:
"@metascraper/helpers" "^3.9.2"
lodash "~4.17.4"
metascraper-url@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/metascraper-url/-/metascraper-url-3.9.2.tgz#e578eb35a13f359dadb57f90d49025a2ed39b43c"
dependencies:
"@metascraper/helpers" "^3.9.2"
metascraper-video@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/metascraper-video/-/metascraper-video-3.9.2.tgz#027c16cf48a48b636c7611492357ef6898e28577"
dependencies:
"@metascraper/helpers" "^3.9.2"
video-extensions "~1.1.0"
metascraper@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/metascraper/-/metascraper-3.9.2.tgz#bc2d1705a80be619ddc254716d2bf9d98e92b1c2"
dependencies:
"@metascraper/helpers" "^3.9.2"
cheerio "~1.0.0-rc.2"
cosmiconfig "~4.0.0"
lodash "~4.17.4"
metascraper-author "^3.9.2"
metascraper-date "^3.3.0"
metascraper-description "^3.9.2"
metascraper-image "^3.9.2"
metascraper-lang "^3.9.0"
metascraper-logo "^3.9.2"
metascraper-publisher "^3.4.0"
metascraper-title "^3.9.2"
metascraper-url "^3.9.2"
metascraper-video "^3.9.2"
p-reduce "~1.0.0"
resolve-from "~4.0.0"
sanitize-html "~1.18.2"
methods@^1.1.1, methods@^1.1.2, methods@~1.1.2:
version "1.1.2"
@@ -7041,22 +7119,12 @@ miller-rabin@^4.0.0:
version "1.33.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
mime-db@~1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.12.0.tgz#3d0c63180f458eb10d325aaa37d7c58ae312e9d7"
mime-types@^2.1.10, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7:
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-types@~2.0.3:
version "2.0.14"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.0.14.tgz#310e159db23e077f8bb22b748dabfa4957140aa6"
dependencies:
mime-db "~1.12.0"
mime@1.4.1, mime@^1.3.4, mime@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
@@ -7699,6 +7767,14 @@ normalize-url@^1.4.0:
query-string "^4.1.0"
sort-keys "^1.0.0"
normalize-url@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6"
dependencies:
prepend-http "^2.0.0"
query-string "^5.0.1"
sort-keys "^2.0.0"
npm-path@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.4.tgz#c641347a5ff9d6a09e4d9bce5580c4f505278e64"
@@ -7969,6 +8045,10 @@ p-map@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
p-reduce@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
p-try@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
@@ -8295,19 +8375,6 @@ pn@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
popsicle@^6.2.0:
version "6.2.2"
resolved "https://registry.yarnpkg.com/popsicle/-/popsicle-6.2.2.tgz#e273c8bd48181f73a59b199e2a5e25b692b3e237"
dependencies:
any-promise "^1.3.0"
arrify "^1.0.0"
concat-stream "^1.4.7"
form-data "^0.2.0"
make-error-cause "^1.0.1"
throwback "^1.1.0"
tough-cookie "^2.0.0"
xtend "^4.0.0"
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -8722,6 +8789,14 @@ postcss@^6.0.1:
source-map "^0.6.1"
supports-color "^4.4.0"
postcss@^6.0.14:
version "6.0.21"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.21.tgz#8265662694eddf9e9a5960db6da33c39e4cd069d"
dependencies:
chalk "^2.3.2"
source-map "^0.6.1"
supports-color "^5.3.0"
postinstall-build@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.1.tgz#b917a9079b26178d9a24af5a5cd8cb4a991d11b9"
@@ -8774,6 +8849,10 @@ prepend-http@^1.0.0, prepend-http@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
prepend-http@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
@@ -9072,6 +9151,14 @@ query-string@^5.0.0:
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
query-string@^5.0.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
dependencies:
decode-uri-component "^0.2.0"
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -9759,6 +9846,10 @@ resolve-from@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57"
resolve-from@~4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -9903,6 +9994,21 @@ sane@^2.0.0:
optionalDependencies:
fsevents "^1.1.1"
sanitize-html@~1.18.2:
version "1.18.2"
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.18.2.tgz#61877ba5a910327e42880a28803c2fbafa8e4642"
dependencies:
chalk "^2.3.0"
htmlparser2 "^3.9.0"
lodash.clonedeep "^4.5.0"
lodash.escaperegexp "^4.1.2"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.mergewith "^4.6.0"
postcss "^6.0.14"
srcset "^1.0.0"
xtend "^4.0.0"
sass-graph@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
@@ -10230,6 +10336,10 @@ smart-buffer@^1.0.13, smart-buffer@^1.0.4:
version "1.1.15"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16"
smartquotes@~2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/smartquotes/-/smartquotes-2.3.1.tgz#01ebb595d6c7a9e24d90e8cb95c17d0e1af49407"
smoothscroll-polyfill@^0.3.5:
version "0.3.6"
resolved "https://registry.yarnpkg.com/smoothscroll-polyfill/-/smoothscroll-polyfill-0.3.6.tgz#492be845195157cdc2fc529a95d89e7a71509172"
@@ -10314,6 +10424,12 @@ sort-keys@^1.0.0:
dependencies:
is-plain-obj "^1.0.0"
sort-keys@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128"
dependencies:
is-plain-obj "^1.0.0"
source-list-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
@@ -10398,6 +10514,13 @@ sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
srcset@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef"
dependencies:
array-uniq "^1.0.2"
number-is-nan "^1.0.0"
sshpk@^1.7.0:
version "1.13.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
@@ -10701,6 +10824,12 @@ supports-color@^5.2.0:
dependencies:
has-flag "^3.0.0"
supports-color@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0"
dependencies:
has-flag "^3.0.0"
svgo@^0.7.0:
version "0.7.2"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
@@ -10849,12 +10978,6 @@ through@2, "through@>=2.2.7 <3", through@^2.3.6, through@~2.3, through@~2.3.1:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
throwback@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/throwback/-/throwback-1.1.1.tgz#f007e7c17604a6d16d7a07c41aa0e8fedc6184a4"
dependencies:
any-promise "^1.3.0"
thunkify@~2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/thunkify/-/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d"
@@ -10899,7 +11022,7 @@ titlecase@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/titlecase/-/titlecase-1.1.2.tgz#78113d1108086b8326331a3247dea8f5a49ea853"
tlds@^1.196.0, tlds@^1.57.0:
tlds@^1.187.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"
@@ -10968,7 +11091,7 @@ to-space-case@^1.0.0:
dependencies:
to-no-case "^1.0.0"
to-title-case@^1.0.0:
to-title-case@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/to-title-case/-/to-title-case-1.0.0.tgz#aca88f89d6064de50108a97cea0db44827e80061"
dependencies:
@@ -10999,7 +11122,7 @@ touch@^3.1.0:
dependencies:
nopt "~1.0.10"
tough-cookie@>=2.3.3, tough-cookie@^2.0.0, tough-cookie@^2.2.0, tough-cookie@^2.3.2, tough-cookie@^2.3.3, tough-cookie@~2.3.0, tough-cookie@~2.3.3:
tough-cookie@>=2.3.3, tough-cookie@^2.2.0, tough-cookie@^2.3.2, tough-cookie@^2.3.3, tough-cookie@~2.3.0, tough-cookie@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
dependencies:
@@ -11293,6 +11416,13 @@ url-parse-lax@^1.0.0:
dependencies:
prepend-http "^1.0.1"
url-regex@~4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/url-regex/-/url-regex-4.1.1.tgz#a5617b22e15e26dac57ce74c3f52088bcdfec995"
dependencies:
ip-regex "^1.0.1"
tlds "^1.187.0"
url-search-params@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/url-search-params/-/url-search-params-0.9.0.tgz#e71d7764a6503533cbfe9771b2963cb61ea1c225"
@@ -11371,6 +11501,10 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
video-extensions@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/video-extensions/-/video-extensions-1.1.0.tgz#eaa86b45f29a853c2b873e9d8e23b513712997d6"
vm-browserify@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"