Merge branch 'master' into gdpr-download

This commit is contained in:
Wyatt Johnson
2018-04-09 16:09:53 -06:00
committed by GitHub
23 changed files with 328 additions and 91 deletions
+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,50 +1,37 @@
import React, { Component } from 'react';
import { Button, List, Item } from 'coral-ui';
import styles from './Configure.css';
import StreamSettings from '../containers/StreamSettings';
import ModerationSettings from '../containers/ModerationSettings';
import TechSettings from '../containers/TechSettings';
import t from 'coral-framework/services/i18n';
import { can } from 'coral-framework/services/perms';
import React from 'react';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import { Button, List, Item } from 'coral-ui';
import { can } from 'coral-framework/services/perms';
import styles from './Configure.css';
import SaveChangesDialog from './SaveChangesDialog';
export default class Configure extends Component {
getSectionComponent(section) {
switch (section) {
case 'stream':
return StreamSettings;
case 'moderation':
return ModerationSettings;
case 'tech':
return TechSettings;
}
throw new Error(`Unknown section ${section}`);
}
class Configure extends React.Component {
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>
@@ -74,10 +61,7 @@ export default class Configure extends Component {
</div>
</div>
<div className={styles.mainContent}>
<SectionComponent
root={this.props.root}
settings={this.props.settings}
/>
{React.cloneElement(this.props.children, passProps)}
</div>
</div>
);
@@ -86,10 +70,17 @@ export default class Configure extends 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;
@@ -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,15 +10,70 @@ 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 { 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>;
@@ -30,14 +85,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>
);
}
}
@@ -74,18 +135,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,
@@ -93,14 +157,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,
};
@@ -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 -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:
@@ -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
+7
View File
@@ -154,12 +154,19 @@ 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"
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"
+7
View File
@@ -153,12 +153,19 @@ 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"
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"
@@ -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;
@@ -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>
);
}
}