mirror of
https://github.com/wassname/talk.git
synced 2026-06-30 21:12:19 +08:00
Merge branch 'master' into story-138187767-mod-flag-names
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-async-to-generator",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error",
|
||||
2
|
||||
|
||||
+1
-4
@@ -1,7 +1,4 @@
|
||||
FROM node:7
|
||||
|
||||
# Install yarn
|
||||
RUN npm install -g yarn
|
||||
FROM node:7.6
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Talk [](https://circleci.com/gh/coralproject/talk)
|
||||
|
||||
A commenting platform from [The Coral Project](https://coralproject.net). Talk enters a closed beta in March 2017, but you can download the code for our alpha here. [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
A commenting platform from [The Coral Project](https://coralproject.net). Talk enters a closed beta in March 2017, but you can download the code for our alpha here. [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
|
||||
Third party licenses are available via the `/client/3rdpartylicenses.txt`
|
||||
endpoint when the server is running with built assets.
|
||||
|
||||
## Contributing to Talk
|
||||
|
||||
|
||||
@@ -45,16 +45,15 @@ const session_opts = {
|
||||
secret: process.env.TALK_SESSION_SECRET,
|
||||
httpOnly: true,
|
||||
rolling: true,
|
||||
saveUninitialized: false,
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
resave: true,
|
||||
unset: 'destroy',
|
||||
name: 'talk.sid',
|
||||
cookie: {
|
||||
secure: false,
|
||||
maxAge: 36000000, // 1 hour for expiry.
|
||||
maxAge: 8.64e+7, // 24 hours for session token expiry
|
||||
},
|
||||
store: new RedisStore({
|
||||
ttl: 1800,
|
||||
client: redis.createClient(),
|
||||
})
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
machine:
|
||||
node:
|
||||
version: 7
|
||||
version: 7.6
|
||||
services:
|
||||
- docker
|
||||
- redis
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../.babelrc",
|
||||
"plugins": [
|
||||
"transform-async-to-generator",
|
||||
]
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error"
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ const routes = (
|
||||
<div>
|
||||
<Route exact path="/admin/install" component={InstallContainer}/>
|
||||
<Route path='/admin' component={LayoutContainer}>
|
||||
<IndexRoute component={ModerationContainer} />
|
||||
|
||||
<IndexRoute component={Dashboard} />
|
||||
<Route path='community' component={CommunityContainer} />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='streams' component={Streams} />
|
||||
<Route path='dashboard' component={Dashboard} />
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
import * as actions from '../constants/auth';
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
import coralApi from 'coral-framework/helpers/response';
|
||||
|
||||
// Log In.
|
||||
export const handleLogin = (email, password) => dispatch => {
|
||||
dispatch({type: actions.LOGIN_REQUEST});
|
||||
return coralApi('/auth/local', {method: 'POST', body: {email, password}})
|
||||
.then(result => {
|
||||
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(checkLoginSuccess(result.user, isAdmin));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch({type: actions.LOGIN_FAILURE, message: error.translation_key});
|
||||
});
|
||||
};
|
||||
|
||||
const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUEST});
|
||||
const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS});
|
||||
const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE});
|
||||
|
||||
export const requestPasswordReset = email => dispatch => {
|
||||
dispatch(forgotPassowordRequest(email));
|
||||
return coralApi('/account/password/reset', {method: 'POST', body: {email}})
|
||||
.then(() => dispatch(forgotPassowordSuccess()))
|
||||
.catch(error => dispatch(forgotPassowordFailure(error)));
|
||||
};
|
||||
|
||||
// Check Login
|
||||
|
||||
@@ -28,7 +52,7 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
|
||||
|
||||
export const logout = () => dispatch => {
|
||||
dispatch(logOutRequest());
|
||||
coralApi('/auth', {method: 'DELETE'})
|
||||
return coralApi('/auth', {method: 'DELETE'})
|
||||
.then(() => dispatch(logOutSuccess()))
|
||||
.catch(error => dispatch(logOutFailure(error)));
|
||||
};
|
||||
|
||||
@@ -20,13 +20,17 @@ const validation = (formData, dispatch, next) => {
|
||||
return dispatch(hasError());
|
||||
}
|
||||
|
||||
const validKeys = Object.keys(formData)
|
||||
.filter(name => name !== 'domains');
|
||||
|
||||
// Required Validation
|
||||
const empty = Object.keys(formData).filter(name => {
|
||||
const empty = validKeys
|
||||
.filter(name => {
|
||||
const cond = !formData[name].length;
|
||||
|
||||
if (cond) {
|
||||
|
||||
// Adding Error
|
||||
// Adding Error
|
||||
dispatch(addError(name, 'This field is required.'));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
@@ -40,18 +44,19 @@ const validation = (formData, dispatch, next) => {
|
||||
}
|
||||
|
||||
// RegExp Validation
|
||||
const validation = Object.keys(formData).filter(name => {
|
||||
const cond = !validate[name](formData[name]);
|
||||
if (cond) {
|
||||
const validation = validKeys
|
||||
.filter(name => {
|
||||
const cond = !validate[name](formData[name]);
|
||||
if (cond) {
|
||||
|
||||
// Adding Error
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
|
||||
return cond;
|
||||
});
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (validation.length) {
|
||||
return dispatch(hasError());
|
||||
@@ -71,23 +76,27 @@ export const submitSettings = () => (dispatch, getState) => {
|
||||
export const submitUser = () => (dispatch, getState) => {
|
||||
const userFormData = getState().install.toJS().data.user;
|
||||
validation(userFormData, dispatch, function() {
|
||||
const data = getState().install.toJS().data;
|
||||
dispatch(installRequest());
|
||||
coralApi('/setup', {method: 'POST', body: data})
|
||||
.then(result => {
|
||||
console.log(result);
|
||||
dispatch(installSuccess());
|
||||
dispatch(nextStep());
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(installFailure(`${error.translation_key}`));
|
||||
});
|
||||
dispatch(nextStep());
|
||||
});
|
||||
};
|
||||
|
||||
export const finishInstall = () => (dispatch, getState) => {
|
||||
const data = getState().install.toJS().data;
|
||||
dispatch(installRequest());
|
||||
return coralApi('/setup', {method: 'POST', body: data})
|
||||
.then(() => {
|
||||
dispatch(installSuccess());
|
||||
dispatch(nextStep());
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(installFailure(`${error.translation_key}`));
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
|
||||
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
|
||||
export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
|
||||
|
||||
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
|
||||
const checkInstallSuccess = installed => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import React from 'react';
|
||||
import styles from './ModerationList.css';
|
||||
import BanUserButton from './BanUserButton';
|
||||
import {FabButton} from 'coral-ui';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../containers/ModerationQueue/helpers/moderationQueueActionsMap';
|
||||
|
||||
const ActionButton = ({type = '', user, ...props}) => {
|
||||
if (type === 'BAN') {
|
||||
return <BanUserButton user={user} onClick={() => props.showBanUserDialog(props.user, props.id)} />;
|
||||
}
|
||||
|
||||
const ActionButton = ({type = '', ...props}) => {
|
||||
return (
|
||||
<FabButton
|
||||
<Button
|
||||
className={`${type.toLowerCase()} ${styles.actionButton}`}
|
||||
cStyle={type.toLowerCase()}
|
||||
icon={menuActionsMap[type].icon}
|
||||
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
|
||||
/>
|
||||
>{menuActionsMap[type].text}</Button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
import styles from './NotFound.css';
|
||||
import {Button, TextField, Alert, Success} from 'coral-ui';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
class AdminLogin extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {email: '', password: '', requestPassword: false};
|
||||
}
|
||||
|
||||
handleSignIn = e => {
|
||||
e.preventDefault();
|
||||
this.props.handleLogin(this.state.email, this.state.password);
|
||||
}
|
||||
|
||||
handleRequestPassword = e => {
|
||||
e.preventDefault();
|
||||
this.props.requestPasswordReset(this.state.email);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {errorMessage} = this.props;
|
||||
const signInForm = (
|
||||
<form onSubmit={this.handleSignIn}>
|
||||
{errorMessage && <Alert>{lang.t(`errors.${errorMessage}`)}</Alert>}
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({email: e.target.value})} />
|
||||
<TextField
|
||||
label='Password'
|
||||
value={this.state.password}
|
||||
onChange={e => this.setState({password: e.target.value})}
|
||||
type='password' />
|
||||
<div style={{height: 10}}></div>
|
||||
<Button
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
full
|
||||
onClick={this.handleSignIn}>Sign In</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={e => {
|
||||
e.preventDefault();
|
||||
this.setState({requestPassword: true});
|
||||
}}>Request a new one.</a>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
const requestPasswordForm = (
|
||||
this.props.passwordRequestSuccess
|
||||
? <p className={styles.passwordRequestSuccess} onClick={() => {
|
||||
location.href = location.href;
|
||||
}}>
|
||||
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
|
||||
<Success />
|
||||
</p>
|
||||
: <form onSubmit={this.handleRequestPassword}>
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({email: e.target.value})} />
|
||||
<Button
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
full
|
||||
onClick={this.handleRequestPassword}>Reset Password</Button>
|
||||
</form>
|
||||
);
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={styles.loginLayout}>
|
||||
<h1 className={styles.loginHeader}>Team sign in</h1>
|
||||
<p className={styles.loginCTA}>Sign in to interact with your community.</p>
|
||||
{ this.state.requestPassword ? requestPasswordForm : signInForm }
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AdminLogin.propTypes = {
|
||||
handleLogin: PropTypes.func.isRequired,
|
||||
passwordRequestSuccess: PropTypes.string,
|
||||
loginError: PropTypes.string
|
||||
};
|
||||
|
||||
export default AdminLogin;
|
||||
@@ -1,10 +1,11 @@
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
-webkit-transform: scale(.8);
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const lang = new I18n(translations);
|
||||
|
||||
const BanUserButton = ({user, ...props}) => (
|
||||
<div className={styles.ban}>
|
||||
<Button cStyle='darkGrey'
|
||||
<Button cStyle='ban'
|
||||
className={`ban ${styles.banButton}`}
|
||||
disabled={user.status === 'BANNED' ? 'disabled' : ''}
|
||||
onClick={props.onClick}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
import timeago from 'timeago.js';
|
||||
import Linkify from 'react-linkify';
|
||||
|
||||
import styles from './ModerationList.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import {Icon} from 'coral-ui';
|
||||
import ActionButton from './ActionButton';
|
||||
|
||||
const linkify = new Linkify();
|
||||
|
||||
// Render a single comment for the list
|
||||
const Comment = props => {
|
||||
const {comment, author} = props;
|
||||
let authorStatus = author.status;
|
||||
const links = linkify.getMatches(comment.body);
|
||||
|
||||
return (
|
||||
<li tabIndex={props.index} className={`mdl-card mdl-shadow--2dp ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<span>{author.username || lang.t('comment.anon')}</span>
|
||||
<span className={styles.created}>{timeago().format(comment.createdAt || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
|
||||
{comment.flagged ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
{links ?
|
||||
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{props.modActions.map(
|
||||
(action, i) =>
|
||||
<ActionButton
|
||||
option={action}
|
||||
key={i}
|
||||
type='COMMENTS'
|
||||
comment={comment}
|
||||
user={author}
|
||||
menuOptionsMap={props.menuOptionsMap}
|
||||
onClickAction={props.onClickAction}
|
||||
onClickShowBanDialog={props.onClickShowBanDialog}/>
|
||||
)}
|
||||
</div>
|
||||
{authorStatus === 'banned' ?
|
||||
<span className={styles.banned}><Icon name='error_outline'/> {lang.t('comment.banned_user')}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.itemBody}>
|
||||
<span className={styles.body}>
|
||||
<Linkify component='span' properties={{style: linkStyles}}>
|
||||
<Highlighter
|
||||
searchWords={props.suspectWords}
|
||||
textToHighlight={comment.body} />
|
||||
</Linkify>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
|
||||
const linkStyles = {
|
||||
backgroundColor: 'rgb(255, 219, 135)',
|
||||
padding: '1px 2px'
|
||||
};
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -1,66 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './FlagWidget.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const FlagWidget = ({assets}) => {
|
||||
|
||||
return (
|
||||
<table className={styles.widgetTable}>
|
||||
<thead className={styles.widgetHead}>
|
||||
<tr>
|
||||
<th></th>{/* empty on purpose */}
|
||||
<th>{lang.t('streams.article')}</th>
|
||||
<th>{lang.t('modqueue.flagged')}</th>
|
||||
<th>{lang.t('modqueue.likes')}</th>
|
||||
<th>{lang.t('dashboard.comment_count')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
assets.length
|
||||
? assets.map((asset, index) => {
|
||||
const flagSummary = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary');
|
||||
const likeSummary = asset.action_summaries.find(s => s.__typename === 'LikeAssetActionSummary');
|
||||
return (
|
||||
<tr key={asset.id}>
|
||||
<td>{index + 1}.</td>
|
||||
<td>
|
||||
<Link to={`/admin/moderate/flagged/${asset.id}`}>{asset.title}</Link>
|
||||
<p className={styles.lede}>{asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</td>
|
||||
<td>{flagSummary ? flagSummary.actionCount : 0}</td>
|
||||
<td>{likeSummary ? likeSummary.actionCount : 0}</td>
|
||||
<td>{asset.commentCount}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: <tr><td colSpan="3">{lang.t('dashboard.no_flags')}</td></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
FlagWidget.propTypes = {
|
||||
assets: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
commentCount: PropTypes.number,
|
||||
action_summaries: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
__typename: PropTypes.string.isRequired,
|
||||
actionCount: PropTypes.number.isRequired,
|
||||
actionableItemCount: PropTypes.number.isRequired
|
||||
})
|
||||
)
|
||||
})
|
||||
).isRequired
|
||||
};
|
||||
|
||||
export default FlagWidget;
|
||||
@@ -178,6 +178,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
|
||||
@@ -1,12 +1,37 @@
|
||||
.layout {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loginLayout {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loginHeader, .loginCTA, .forgotPasswordCTA, .passwordRequestSuccess {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.forgotPasswordLink, .signInLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgotPasswordLink:hover, .signInLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.layout img {
|
||||
width: 100%;
|
||||
.loginHeader {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
cursor: pointer;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Layout} from 'react-mdl';
|
||||
import styles from './NotFound.css';
|
||||
|
||||
export const PermissionRequired = () => (
|
||||
<Layout fixedDrawer>
|
||||
<div className={styles.layout} >
|
||||
<h1>Permission Required</h1>
|
||||
<p>We’re sorry, but you don’t have access to that page.</p>
|
||||
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Navigation, Drawer} from 'react-mdl';
|
||||
import {IndexLink, Link} from 'react-router';
|
||||
import styles from './Drawer.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
|
||||
export default ({handleLogout, restricted = false}) => (
|
||||
const CoralDrawer = ({handleLogout, restricted = false}) => (
|
||||
<Drawer className={styles.header}>
|
||||
{ !restricted ?
|
||||
<div>
|
||||
@@ -45,5 +45,11 @@ export default ({handleLogout, restricted = false}) => (
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
CoralDrawer.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool // hide app elements from a logged out user
|
||||
};
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default CoralDrawer;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
|
||||
import {Link, IndexLink} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
@@ -6,7 +6,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import {Logo} from './Logo';
|
||||
|
||||
export default ({handleLogout, restricted = false}) => (
|
||||
const CoralHeader = ({handleLogout, restricted = false}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
{
|
||||
@@ -14,28 +14,35 @@ export default ({handleLogout, restricted = false}) => (
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
<Link
|
||||
id='moderateNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.moderate')}
|
||||
</Link>
|
||||
<Link className={styles.navLink}
|
||||
<Link
|
||||
id='streamsNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/streams"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.streams')}
|
||||
</Link>
|
||||
<Link className={styles.navLink}
|
||||
<Link
|
||||
id='communityNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.community')}
|
||||
</Link>
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
@@ -64,4 +71,11 @@ export default ({handleLogout, restricted = false}) => (
|
||||
</Header>
|
||||
);
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool // hide elemnts from a user that's logged out
|
||||
};
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default CoralHeader;
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Layout as LayoutMDL} from 'react-mdl';
|
||||
import Header from './Header';
|
||||
import Drawer from './Drawer';
|
||||
import styles from './Layout.css';
|
||||
|
||||
export const Layout = ({children, ...props}) => (
|
||||
const Layout = ({children, handleLogout = () => {}, restricted = false, ...props}) => (
|
||||
<LayoutMDL fixedDrawer>
|
||||
<Header {...props} />
|
||||
<Drawer {...props} />
|
||||
<div className={styles.layout} >
|
||||
<Header handleLogout={handleLogout} restricted={restricted} {...props} />
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} {...props} />
|
||||
<div className={styles.layout}>
|
||||
{children}
|
||||
</div>
|
||||
</LayoutMDL>
|
||||
);
|
||||
|
||||
Layout.propTypes = {
|
||||
handleLogout: PropTypes.func,
|
||||
restricted: PropTypes.bool // hide elements from a user that's logged out
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
background: #E5E5E5;
|
||||
height: 100%;
|
||||
width: 128px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.base {
|
||||
|
||||
@@ -7,3 +7,11 @@ export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN';
|
||||
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
|
||||
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
|
||||
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
|
||||
|
||||
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
|
||||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
||||
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
|
||||
|
||||
export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
|
||||
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
|
||||
export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE';
|
||||
|
||||
@@ -10,6 +10,7 @@ export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
|
||||
export const INSTALL_FAILURE = 'INSTALL_FAILURE';
|
||||
export const UPDATE_FORMDATA_USER = 'UPDATE_FORMDATA_USER';
|
||||
export const UPDATE_FORMDATA_SETTINGS = 'UPDATE_FORMDATA_SETTINGS';
|
||||
export const UPDATE_PERMITTED_DOMAINS_SETTINGS = 'UPDATE_PERMITTED_DOMAINS_SETTINGS';
|
||||
|
||||
export const CHECK_INSTALL_REQUEST = 'CHECK_INSTALL_REQUEST';
|
||||
export const CHECK_INSTALL_SUCCESS = 'CHECK_INSTALL_SUCCESS';
|
||||
|
||||
@@ -97,7 +97,7 @@ class CommunityContainer extends Component {
|
||||
return (
|
||||
<div>
|
||||
<FlaggedAccounts
|
||||
commenters={data.usersFlagged}
|
||||
commenters={data.users}
|
||||
isFetching={data.loading}
|
||||
error={data.error}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
h3 {
|
||||
color: black;
|
||||
font-size: 1.76em;
|
||||
font-size: 1.26em;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,10 @@
|
||||
margin-bottom: 20px;
|
||||
align-items: flex-start;
|
||||
min-height: 100px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.settingsError {
|
||||
@@ -118,18 +122,19 @@
|
||||
}
|
||||
|
||||
#bannedWordlist, #suspectWordlist {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
|
||||
display: block;
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.wordlistHeader {
|
||||
font-weight: bold;
|
||||
font-size:18px;
|
||||
margin-bottom:3px;
|
||||
.customCSSInput {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
padding: 14px;
|
||||
letter-spacing: 0.03em;
|
||||
color: #555;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.enabledSetting {
|
||||
@@ -150,7 +155,7 @@
|
||||
margin-top: 38px;
|
||||
}
|
||||
|
||||
.commentSettingsSection {
|
||||
.settingsSection {
|
||||
padding-bottom: 200px;
|
||||
.action {
|
||||
display: inline-block;
|
||||
|
||||
@@ -8,22 +8,20 @@ import {
|
||||
updateDomainlist
|
||||
} from '../../actions/settings';
|
||||
|
||||
import {Button, List, Item} from 'coral-ui';
|
||||
import {Button, List, Item, Card, Spinner} from 'coral-ui';
|
||||
import styles from './Configure.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import EmbedLink from './EmbedLink';
|
||||
import CommentSettings from './CommentSettings';
|
||||
import Wordlist from './Wordlist';
|
||||
import Domainlist from './Domainlist';
|
||||
import has from 'lodash/has';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
import StreamSettings from './StreamSettings';
|
||||
import ModerationSettings from './ModerationSettings';
|
||||
import TechSettings from './TechSettings';
|
||||
|
||||
class Configure extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeSection: 'comments',
|
||||
activeSection: 'stream',
|
||||
changed: false,
|
||||
errors: {}
|
||||
};
|
||||
@@ -70,40 +68,48 @@ class Configure extends Component {
|
||||
|
||||
getSection (section) {
|
||||
const pageTitle = this.getPageTitle(section);
|
||||
let sectionComponent;
|
||||
switch(section){
|
||||
case 'comments':
|
||||
return <CommentSettings
|
||||
title={pageTitle}
|
||||
fetchingSettings={this.props.fetchingSettings}
|
||||
case 'stream':
|
||||
sectionComponent = <StreamSettings
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate}
|
||||
errors={this.state.errors}
|
||||
settingsError={this.onSettingError}/>;
|
||||
case 'embed':
|
||||
return has(this, 'props.settings.domains.whitelist')
|
||||
? <div>
|
||||
<Domainlist
|
||||
domains={this.props.settings.domains.whitelist}
|
||||
onChangeDomainlist={this.onChangeDomainlist}/>
|
||||
<EmbedLink title={pageTitle} />
|
||||
</div>
|
||||
: <EmbedLink title={pageTitle} />;
|
||||
case 'wordlist':
|
||||
return has(this, 'props.settings.wordlist')
|
||||
? <Wordlist
|
||||
bannedWords={this.props.settings.wordlist.banned}
|
||||
suspectWords={this.props.settings.wordlist.suspect}
|
||||
onChangeWordlist={this.onChangeWordlist} />
|
||||
: <p>loading wordlists</p>;
|
||||
break;
|
||||
case 'moderation':
|
||||
sectionComponent = <ModerationSettings
|
||||
onChangeWordlist={this.onChangeWordlist}
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate} />;
|
||||
break;
|
||||
case 'tech':
|
||||
sectionComponent = <TechSettings
|
||||
onChangeDomainlist={this.onChangeDomainlist}
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate} />;
|
||||
}
|
||||
|
||||
if (this.props.settings.fetchingSettings) {
|
||||
return <Card shadow="4"><Spinner/>Loading settings...</Card>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.settingsSection}>
|
||||
<h3>{pageTitle}</h3>
|
||||
{sectionComponent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getPageTitle (section) {
|
||||
switch(section) {
|
||||
case 'comments':
|
||||
return lang.t('configure.comment-settings');
|
||||
case 'embed':
|
||||
return lang.t('configure.embed-comment-stream');
|
||||
case 'stream':
|
||||
return lang.t('configure.stream-settings');
|
||||
case 'moderation':
|
||||
return lang.t('configure.moderation-settings');
|
||||
case 'tech':
|
||||
return lang.t('configure.tech-settings');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -120,14 +126,14 @@ class Configure extends Component {
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<List onChange={this.changeSection} activeItem={activeSection}>
|
||||
<Item itemId='comments' icon="settings">
|
||||
{lang.t('configure.comment-settings')}
|
||||
<Item itemId='stream' icon='speaker_notes'>
|
||||
{lang.t('configure.stream-settings')}
|
||||
</Item>
|
||||
<Item itemId='embed' icon='code'>
|
||||
{lang.t('configure.embed-comment-stream')}
|
||||
<Item itemId='moderation' icon='thumbs_up_down'>
|
||||
{lang.t('configure.moderation-settings')}
|
||||
</Item>
|
||||
<Item itemId='wordlist' icon='settings'>
|
||||
{lang.t('configure.wordlist')}
|
||||
<Item itemId='tech' icon='code'>
|
||||
{lang.t('configure.tech-settings')}
|
||||
</Item>
|
||||
</List>
|
||||
<div className={styles.saveBox}>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React from 'react';
|
||||
import {Card} from 'coral-ui';
|
||||
import styles from './Configure.css';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
import styles from './Configure.css';
|
||||
import {Card} from 'coral-ui';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const Domainlist = ({domains, onChangeDomainlist}) => (
|
||||
<div>
|
||||
<h3>{lang.t('configure.domain-list-title')}</h3>
|
||||
<Card id={styles.domainlist}>
|
||||
const Domainlist = ({domains, onChangeDomainlist}) => {
|
||||
return (
|
||||
<Card id={styles.domainlist} className={styles.configSetting}>
|
||||
<h3>{lang.t('configure.domain-list-title')}</h3>
|
||||
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
|
||||
<TagsInput
|
||||
value={domains}
|
||||
@@ -18,9 +20,7 @@ const Domainlist = ({domains, onChangeDomainlist}) => (
|
||||
onChange={tags => onChangeDomainlist('whitelist', tags)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Domainlist;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -44,19 +44,15 @@ class EmbedLink extends Component {
|
||||
"></script>
|
||||
`.trim();
|
||||
return (
|
||||
<div>
|
||||
<h3>{this.props.title}</h3>
|
||||
<div>
|
||||
<Card shadow="2">
|
||||
<p>{lang.t('configure.copy-and-paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
|
||||
{lang.t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<Card shadow="2" className={styles.configSetting}>
|
||||
<h3>Embed Comment Stream</h3>
|
||||
<p>{lang.t('configure.copy-and-paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
|
||||
{lang.t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './Configure.css';
|
||||
import {Card} from 'coral-ui';
|
||||
import {Checkbox} from 'react-mdl';
|
||||
import Wordlist from './Wordlist';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const updateModeration = (updateSettings, mod) => () => {
|
||||
const moderation = mod === 'PRE' ? 'POST' : 'PRE';
|
||||
updateSettings({moderation});
|
||||
};
|
||||
|
||||
const updateEmailConfirmation = (updateSettings, verify) => () => {
|
||||
updateSettings({requireEmailConfirmation: !verify});
|
||||
};
|
||||
|
||||
const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => {
|
||||
|
||||
// just putting this here for shorthand below
|
||||
const on = styles.enabledSetting;
|
||||
const off = styles.disabledSetting;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateEmailConfirmation(updateSettings, settings.requireEmailConfirmation)}
|
||||
checked={settings.requireEmailConfirmation} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.require-email-verification')}</div>
|
||||
<p className={settings.requireEmailConfirmation ? '' : styles.disabledSettingText}>
|
||||
{lang.t('configure.require-email-verification-text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateModeration(updateSettings, settings.moderation)}
|
||||
checked={settings.moderation === 'PRE'} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
|
||||
<p className={settings.moderation === 'PRE' ? '' : styles.disabledSettingText}>
|
||||
{lang.t('configure.enable-pre-moderation-text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Wordlist
|
||||
bannedWords={settings.wordlist.banned}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
onChangeWordlist={onChangeWordlist} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ModerationSettings.propTypes = {
|
||||
onChangeWordlist: PropTypes.func.isRequired,
|
||||
settings: PropTypes.shape({
|
||||
moderation: PropTypes.string.isRequired,
|
||||
wordlist: PropTypes.shape({
|
||||
banned: PropTypes.array.isRequired,
|
||||
suspect: PropTypes.array.isRequired
|
||||
})
|
||||
}).isRequired,
|
||||
updateSettings: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ModerationSettings;
|
||||
+4
-60
@@ -4,7 +4,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
import {Textfield, Checkbox} from 'react-mdl';
|
||||
import {Card, Icon, Spinner} from 'coral-ui';
|
||||
import {Card, Icon} from 'coral-ui';
|
||||
|
||||
const TIMESTAMPS = {
|
||||
weeks: 60 * 60 * 24 * 7,
|
||||
@@ -27,15 +27,6 @@ const updateCharCount = (updateSettings, settingsError) => (event) => {
|
||||
updateSettings({charCount: charCount});
|
||||
};
|
||||
|
||||
const updateModeration = (updateSettings, mod) => () => {
|
||||
const moderation = mod === 'PRE' ? 'POST' : 'PRE';
|
||||
updateSettings({moderation});
|
||||
};
|
||||
|
||||
const updateEmailConfirmation = (updateSettings, verify) => () => {
|
||||
updateSettings({requireEmailConfirmation: !verify});
|
||||
};
|
||||
|
||||
const updateInfoBoxEnable = (updateSettings, infoBox) => () => {
|
||||
const infoBoxEnable = !infoBox;
|
||||
updateSettings({infoBoxEnable});
|
||||
@@ -51,11 +42,6 @@ const updateClosedMessage = (updateSettings) => (event) => {
|
||||
updateSettings({closedMessage});
|
||||
};
|
||||
|
||||
const updateCustomCssUrl = (updateSettings) => (event) => {
|
||||
const customCssUrl = event.target.value;
|
||||
updateSettings({customCssUrl});
|
||||
};
|
||||
|
||||
// If we are changing the measure we need to recalculate using the old amount
|
||||
// Same thing if we are just changing the amount
|
||||
const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
|
||||
@@ -71,44 +57,14 @@ const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
|
||||
}
|
||||
};
|
||||
|
||||
const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError, settings, errors}) => {
|
||||
if (fetchingSettings) {
|
||||
return <Card shadow="4"><Spinner/>Loading settings...</Card>;
|
||||
}
|
||||
const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
|
||||
|
||||
// just putting this here for shorthand below
|
||||
const on = styles.enabledSetting;
|
||||
const off = styles.disabledSetting;
|
||||
|
||||
return (
|
||||
<div className={styles.commentSettingsSection}>
|
||||
<h3>{title}</h3>
|
||||
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateModeration(updateSettings, settings.moderation)}
|
||||
checked={settings.moderation === 'PRE'} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
|
||||
<p className={settings.moderation === 'PRE' ? '' : styles.disabledSettingText}>
|
||||
{lang.t('configure.enable-pre-moderation-text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateEmailConfirmation(updateSettings, settings.requireEmailConfirmation)}
|
||||
checked={settings.requireEmailConfirmation} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.require-email-verification')}</div>
|
||||
<p className={settings.requireEmailConfirmation ? '' : styles.disabledSettingText}>
|
||||
{lang.t('configure.require-email-verification-text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<div>
|
||||
<Card className={`${styles.configSetting} ${settings.charCountEnable ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
@@ -193,23 +149,11 @@ const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={styles.configSettingInfoBox}>
|
||||
<div className={styles.content}>
|
||||
{lang.t('configure.custom-css-url')}
|
||||
<p>{lang.t('configure.custom-css-url-desc')}</p>
|
||||
<br />
|
||||
<Textfield
|
||||
style={{width: '100%'}}
|
||||
label={lang.t('configure.custom-css-url')}
|
||||
value={settings.customCssUrl}
|
||||
onChange={updateCustomCssUrl(updateSettings)} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentSettings;
|
||||
export default StreamSettings;
|
||||
|
||||
// To see if we are talking about weeks, days or hours
|
||||
// We talk the remainder of the division and see if it's 0
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Card} from 'coral-ui';
|
||||
import Domainlist from './Domainlist';
|
||||
import EmbedLink from './EmbedLink';
|
||||
import styles from './Configure.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const updateCustomCssUrl = (updateSettings) => (event) => {
|
||||
const customCssUrl = event.target.value;
|
||||
updateSettings({customCssUrl});
|
||||
};
|
||||
|
||||
const TechSettings = ({settings, onChangeDomainlist, updateSettings}) => {
|
||||
return (
|
||||
<div>
|
||||
<Domainlist
|
||||
domains={settings.domains.whitelist}
|
||||
onChangeDomainlist={onChangeDomainlist} />
|
||||
<EmbedLink />
|
||||
<Card className={styles.configSetting}>
|
||||
<h3>{lang.t('configure.custom-css-url')}</h3>
|
||||
<p>{lang.t('configure.custom-css-url-desc')}</p>
|
||||
<br />
|
||||
<input
|
||||
className={styles.customCSSInput}
|
||||
value={settings.customCssUrl}
|
||||
onChange={updateCustomCssUrl(updateSettings)} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TechSettings.propTypes = {
|
||||
settings: PropTypes.shape({
|
||||
domains: PropTypes.shape({
|
||||
whitelist: PropTypes.array.isRequired
|
||||
})
|
||||
}).isRequired,
|
||||
updateSettings: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default TechSettings;
|
||||
@@ -7,9 +7,8 @@ import {Card} from 'coral-ui';
|
||||
|
||||
const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
|
||||
<div>
|
||||
<h3>{lang.t('configure.banned-words-title')}</h3>
|
||||
<Card id={styles.bannedWordlist}>
|
||||
<p className={styles.wordlistHeader}>{lang.t('configure.banned-word-header')}</p>
|
||||
<Card id={styles.bannedWordlist} className={styles.configSetting}>
|
||||
<h3>{lang.t('configure.banned-words-title')}</h3>
|
||||
<p className={styles.wordlistDesc}>{lang.t('configure.banned-word-text')}</p>
|
||||
<TagsInput
|
||||
value={bannedWords}
|
||||
@@ -19,9 +18,8 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
|
||||
onChange={tags => onChangeWordlist('banned', tags)}
|
||||
/>
|
||||
</Card>
|
||||
<h3>{lang.t('configure.suspect-words-title')}</h3>
|
||||
<Card id={styles.suspectWordlist}>
|
||||
<p className={styles.wordlistHeader}>{lang.t('configure.suspect-word-header')}</p>
|
||||
<Card id={styles.suspectWordlist} className={styles.configSetting}>
|
||||
<h3>{lang.t('configure.suspect-words-title')}</h3>
|
||||
<p className={styles.wordlistDesc}>{lang.t('configure.suspect-word-text')}</p>
|
||||
<TagsInput
|
||||
value={suspectWords}
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
.Dashboard {
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
|
||||
margin-right: 10px;
|
||||
padding: 15px;
|
||||
|
||||
}
|
||||
|
||||
.widget:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.heading {
|
||||
|
||||
@@ -1,38 +1,46 @@
|
||||
import React from 'react';
|
||||
import {compose} from 'react-apollo';
|
||||
import {mostFlags} from 'coral-admin/src/graphql/queries';
|
||||
import {Spinner} from 'coral-ui';
|
||||
import styles from './Dashboard.css';
|
||||
import FlagWidget from '../../components/FlagWidget';
|
||||
import {compose} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import {getMetrics} from 'coral-admin/src/graphql/queries';
|
||||
import FlagWidget from './FlagWidget';
|
||||
import LikeWidget from './LikeWidget';
|
||||
import {showBanUserDialog, hideBanUserDialog} from 'coral-admin/src/actions/moderation';
|
||||
|
||||
import {Spinner} from 'coral-ui';
|
||||
|
||||
class Dashboard extends React.Component {
|
||||
|
||||
render () {
|
||||
|
||||
const {data} = this.props;
|
||||
const {metrics: assets} = data;
|
||||
|
||||
if (data.loading) {
|
||||
if (this.props.data && this.props.data.loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
return <code><pre>{data.error}</pre></code>;
|
||||
}
|
||||
const {data: {assetsByLike, assetsByFlag}} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.Dashboard}>
|
||||
<div className={styles.widget}>
|
||||
<h2 className={styles.heading}>Top Ten Articles with the most flagged comments</h2>
|
||||
<FlagWidget assets={assets} />
|
||||
</div>
|
||||
<div className={styles.widget}>
|
||||
<h2 className={styles.heading}>Top ten comments with the most likes</h2>
|
||||
</div>
|
||||
<FlagWidget assets={assetsByFlag} />
|
||||
<LikeWidget assets={assetsByLike} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
settings: state.settings.toJS(),
|
||||
moderation: state.moderation.toJS()
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)),
|
||||
hideBanUserDialog: () => dispatch(hideBanUserDialog(false))
|
||||
});
|
||||
|
||||
export default compose(
|
||||
mostFlags
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
getMetrics
|
||||
)(Dashboard);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Widget.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const FlagWidget = (props) => {
|
||||
const {assets} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.widget}>
|
||||
<h2 className={styles.heading}>Articles with the most flags</h2>
|
||||
<table className={styles.widgetTable}>
|
||||
<thead className={styles.widgetHead}>
|
||||
<tr>
|
||||
<th>{lang.t('streams.article')}</th>
|
||||
<th>{lang.t('dashboard.flags')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
assets.length
|
||||
? assets.map(asset => {
|
||||
const flagSummary = asset.action_summaries.find(s => s.type === 'FlagAssetActionSummary');
|
||||
return (
|
||||
<tr className={styles.rowLinkify} key={asset.id}>
|
||||
<td>
|
||||
<Link className={styles.linkToAsset} to={`/admin/moderate/flagged/${asset.id}`}>
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link className={styles.linkToAsset} to={`/admin/moderate/flagged/${asset.id}`}>
|
||||
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: <tr className={styles.rowLinkify}><td colSpan="2">{lang.t('dashboard.no_flags')}</td></tr>
|
||||
}
|
||||
{ /* rows in a table with a fixed height will expand and ignore height.
|
||||
this extra row will expand to fill the extra space. */
|
||||
assets.length < 10 ? <tr></tr> : null
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlagWidget;
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Widget.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const LikeWidget = (props) => {
|
||||
|
||||
const {assets} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.widget}>
|
||||
<h2 className={styles.heading}>Articles with the most likes</h2>
|
||||
<table className={styles.widgetTable}>
|
||||
<thead className={styles.widgetHead}>
|
||||
<tr>
|
||||
<th>{lang.t('streams.article')}</th>
|
||||
<th>{lang.t('modqueue.likes')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
assets.length
|
||||
? assets.map(asset => {
|
||||
const likeSummary = asset.action_summaries.find(s => s.type === 'LikeAssetActionSummary');
|
||||
return (
|
||||
<tr className={styles.rowLinkify} key={asset.id}>
|
||||
<td>
|
||||
<Link className={styles.linkToAsset} to={`/admin/moderate/flagged/${asset.id}`}>
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link className={styles.linkToAsset} to={`/admin/moderate/flagged/${asset.id}`}>
|
||||
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: <tr className={styles.rowLinkify}><td colSpan="2">{lang.t('dashboard.no_likes')}</td></tr>
|
||||
}
|
||||
{ /* rows in a table with a fixed height will expand and ignore height.
|
||||
this extra row will expand to fill the extra space. */
|
||||
assets.length < 10 ? <tr></tr> : null
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LikeWidget;
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations';
|
||||
import ModerationQueue from 'coral-admin/src/containers/ModerationQueue/ModerationQueue';
|
||||
import styles from './Widget.css';
|
||||
import BanUserDialog from 'coral-admin/src/components/BanUserDialog';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const MostLikedCommentsWidget = props => {
|
||||
const {
|
||||
comments,
|
||||
moderation,
|
||||
settings,
|
||||
handleBanUser,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
acceptComment,
|
||||
rejectComment
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.widget}>
|
||||
<h2 className={styles.heading}>{lang.t('most_liked_comments')}</h2>
|
||||
<ModerationQueue
|
||||
comments={comments}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
showBanUserDialog={showBanUserDialog}
|
||||
acceptComment={acceptComment}
|
||||
rejectComment={rejectComment} />
|
||||
<BanUserDialog
|
||||
open={moderation.banDialog}
|
||||
user={moderation.user}
|
||||
handleClose={hideBanUserDialog}
|
||||
handleBanUser={handleBanUser} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MostLikedCommentsWidget;
|
||||
@@ -0,0 +1,84 @@
|
||||
:root {
|
||||
--row-height: 80px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
box-sizing: border-box;
|
||||
margin: 10px 5px 5px 5px;
|
||||
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
|
||||
padding: 15px;
|
||||
flex: 1;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0;
|
||||
padding-left: 10px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.widgetTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
user-select: none;
|
||||
height: calc(var(--row-height) * 10);
|
||||
}
|
||||
|
||||
.widgetTable thead th {
|
||||
color: rgb(35, 102, 223);
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.widgetTable thead th:last-child {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.rowLinkify {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid lightgrey;
|
||||
color: #555;
|
||||
height: var(--row-height);
|
||||
}
|
||||
|
||||
.rowLinkify:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.rowLinkify:hover {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.widgetTable tbody td {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.linkToAsset {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.lede {
|
||||
font-size: 0.9em;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.assetTitle {
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
font-size: 1.2em;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.assetTitle:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.widgetCount {
|
||||
color: #555;
|
||||
font-size: 1.3em;
|
||||
font-weight: 400;
|
||||
}
|
||||
@@ -2,22 +2,25 @@ import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import styles from './style.css';
|
||||
import {Wizard, WizardNav} from 'coral-ui';
|
||||
import {Layout} from '../../components/ui/Layout';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
|
||||
import {
|
||||
nextStep,
|
||||
previousStep,
|
||||
goToStep,
|
||||
nextStep,
|
||||
submitUser,
|
||||
checkInstall,
|
||||
previousStep,
|
||||
finishInstall,
|
||||
submitSettings,
|
||||
updateUserFormData,
|
||||
updateSettingsFormData,
|
||||
submitSettings,
|
||||
submitUser,
|
||||
checkInstall
|
||||
updatePermittedDomains
|
||||
} from '../../actions/install';
|
||||
|
||||
import InitialStep from './components/Steps/InitialStep';
|
||||
import AddOrganizationName from './components/Steps/AddOrganizationName';
|
||||
import CreateYourAccount from './components/Steps/CreateYourAccount';
|
||||
import PermittedDomainsStep from './components/Steps/PermittedDomainsStep';
|
||||
import FinalStep from './components/Steps/FinalStep';
|
||||
|
||||
class InstallContainer extends Component {
|
||||
@@ -43,6 +46,7 @@ class InstallContainer extends Component {
|
||||
<InitialStep/>
|
||||
<AddOrganizationName/>
|
||||
<CreateYourAccount/>
|
||||
<PermittedDomainsStep/>
|
||||
<FinalStep/>
|
||||
</Wizard>
|
||||
</div>
|
||||
@@ -65,10 +69,14 @@ const mapStateToProps = state => ({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
checkInstall: next => dispatch(checkInstall(next)),
|
||||
nextStep: () => dispatch(nextStep()),
|
||||
previousStep: () => dispatch(previousStep()),
|
||||
goToStep: step => dispatch(goToStep(step)),
|
||||
previousStep: () => dispatch(previousStep()),
|
||||
finishInstall: () => dispatch(finishInstall()),
|
||||
checkInstall: next => dispatch(checkInstall(next)),
|
||||
handleDomainsChange: value => {
|
||||
dispatch(updatePermittedDomains(value));
|
||||
},
|
||||
handleSettingsChange: e => {
|
||||
const {name, value} = e.currentTarget;
|
||||
dispatch(updateSettingsFormData(name, value));
|
||||
|
||||
@@ -2,25 +2,26 @@ import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {TextField, Button} from 'coral-ui';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const AddOrganizationName = props => {
|
||||
const {handleSettingsChange, handleSettingsSubmit, install} = props;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
<p>
|
||||
Please tell us the name of your organization. This will appear in emails when
|
||||
inviting new team members
|
||||
</p>
|
||||
<p>{lang.t('ADD_ORGANIZATION.DESCRIPTION')}</p>
|
||||
<div className={styles.form}>
|
||||
<form onSubmit={handleSettingsSubmit}>
|
||||
<TextField
|
||||
className={styles.TextField}
|
||||
id="organizationName"
|
||||
type="text"
|
||||
label='Organization name'
|
||||
label={lang.t('ADD_ORGANIZATION.LABEL')}
|
||||
onChange={handleSettingsChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.organizationName} />
|
||||
<Button type="submit" cStyle='black' full>Save</Button>
|
||||
<Button type="submit" cStyle='black' full>{lang.t('ADD_ORGANIZATION.SAVE')}</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,10 @@ import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {TextField, Button, Spinner} from 'coral-ui';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const InitialStep = props => {
|
||||
const {handleUserChange, handleUserSubmit, install} = props;
|
||||
return (
|
||||
@@ -12,7 +16,7 @@ const InitialStep = props => {
|
||||
className={styles.textField}
|
||||
id="email"
|
||||
type="email"
|
||||
label='Email address'
|
||||
label={lang.t('CREATE.EMAIL')}
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.email}
|
||||
@@ -23,7 +27,7 @@ const InitialStep = props => {
|
||||
className={styles.textField}
|
||||
id="username"
|
||||
type="text"
|
||||
label='Username'
|
||||
label={lang.t('CREATE.USERNAME')}
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.username}
|
||||
@@ -33,7 +37,7 @@ const InitialStep = props => {
|
||||
className={styles.textField}
|
||||
id="password"
|
||||
type="password"
|
||||
label='Password'
|
||||
label={lang.t('CREATE.PASSWORD')}
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.password}
|
||||
@@ -43,7 +47,7 @@ const InitialStep = props => {
|
||||
className={styles.textField}
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
label='Confirm Password'
|
||||
label={lang.t('CREATE.CONFIRM_PASSWORD')}
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.confirmPassword}
|
||||
@@ -51,7 +55,7 @@ const InitialStep = props => {
|
||||
|
||||
{
|
||||
!props.install.isLoading ?
|
||||
<Button cStyle='black' type="submit" full>Save</Button>
|
||||
<Button cStyle='black' type="submit" full>{lang.t('CREATE.SAVE')}</Button>
|
||||
:
|
||||
<Spinner />
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@ import styles from './style.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const InitialStep = () => {
|
||||
return (
|
||||
<div className={`${styles.step} ${styles.finalStep}`}>
|
||||
<p>
|
||||
Thanks for installing Talk! We sent an email to verify your email
|
||||
address. While you finish setting the account, you can start engaging
|
||||
with your readers now.
|
||||
</p>
|
||||
<Button raised><Link to='/admin'>Launch Talk</Link></Button>
|
||||
<Button cStyle='black' raised><a href="http://coralproject.net">Close this Installer</a></Button>
|
||||
<p>{lang.t('FINAL.DESCRIPTION')}</p>
|
||||
<Button raised><Link to='/admin'>{lang.t('FINAL.LAUNCH')}</Link></Button>
|
||||
<Button cStyle='black' raised><a href="http://coralproject.net">{lang.t('FINAL.CLOSE')}</a></Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,16 +2,16 @@ import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {Button} from 'coral-ui';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const InitialStep = props => {
|
||||
const {nextStep} = props;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
<p>
|
||||
The remainder of the Talk installation will take about ten minutes.
|
||||
Once you complete the following two steps, you will have a free
|
||||
installation and provision of Mongo and Redis.
|
||||
</p>
|
||||
<Button cStyle='green' onClick={nextStep} raised>Get Started</Button>
|
||||
<p>{lang.t('INITIAL.DESCRIPTION')}</p>
|
||||
<Button cStyle='green' onClick={nextStep} raised>{lang.t('INITIAL.SUBMIT')}</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {Button, Card} from 'coral-ui';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const PermittedDomainsStep = props => {
|
||||
const {finishInstall, install, handleDomainsChange} = props;
|
||||
const domains = install.data.settings.domains.whitelist;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
<h3>{lang.t('PERMITTED_DOMAINS.TITLE')}</h3>
|
||||
<Card className={styles.card}>
|
||||
<p>{lang.t('PERMITTED_DOMAINS.DESCRIPTION')}</p>
|
||||
<TagsInput
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => handleDomainsChange(tags)}
|
||||
/>
|
||||
</Card>
|
||||
<Button cStyle='green' onClick={finishInstall} raised>{lang.t('PERMITTED_DOMAINS.SUBMIT')}</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermittedDomainsStep;
|
||||
@@ -59,6 +59,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
max-width: 500px;
|
||||
margin: 20px auto;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.finalStep {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"en": {
|
||||
"INITIAL" : {
|
||||
"DESCRIPTION": "Let's set up your Talk community in just a few short steps.",
|
||||
"SUBMIT": "Get Started"
|
||||
},
|
||||
"ADD_ORGANIZATION": {
|
||||
"DESCRIPTION": "Please tell us the name of your organization. This will appear in emails when inviting new team members.",
|
||||
"LABEL": "Organization Name",
|
||||
"SAVE": "Save"
|
||||
},
|
||||
"CREATE": {
|
||||
"EMAIL": "Email address",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"CONFIRM_PASSWORD": "Confirm Password",
|
||||
"SAVE": "Save"
|
||||
},
|
||||
"PERMITTED_DOMAINS": {
|
||||
"TITLE": "Permitted domains",
|
||||
"DESCRIPTION": "Enter the domains you would like to permit for Talk, e.g. your local, staging and production environments (ex. localhost:3000, staging.domain.com, domain.com).",
|
||||
"SUBMIT": "Finish install"
|
||||
},
|
||||
"FINAL": {
|
||||
"DESCRIPTION": "Thanks for installing Talk! We sent an email to verify your email address. While you finish setting up the account, you can start engaging with your readers now.",
|
||||
"LAUNCH": "Launch Talk",
|
||||
"CLOSE": "Close this Installer"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"INITIAL" : {
|
||||
"DESCRIPTION": "Configuremos tu comunidad de Talk en sólo algunos pasos.",
|
||||
"SUBMIT": "Empezá!"
|
||||
},
|
||||
"ADD_ORGANIZATION": {
|
||||
"DESCRIPTION": "Por favor, dinos el nombre de tu organización. Este aparecerá en los emails cuando invites nuevos miembros.",
|
||||
"LABEL": "Nombre de la Organización",
|
||||
"SAVE": "Guardar"
|
||||
},
|
||||
"CREATE": {
|
||||
"EMAIL": "Dirección de E-Mail",
|
||||
"USERNAME": "Usuario",
|
||||
"PASSWORD": "Contraseña",
|
||||
"CONFIRM_PASSWORD": "Confirmar contraseña",
|
||||
"SAVE": "Guardar"
|
||||
},
|
||||
"PERMITTED_DOMAINS": {
|
||||
"TITLE": "Dominios Permitidos",
|
||||
"DESCRIPTION": "Agrega dominios permitidos a Talk, e.g. tu localhost, staging y ambientes de production (ex. localhost:3000, staging.domain.com, domain.com).",
|
||||
"SUBMIT": "Finalizar instalación"
|
||||
},
|
||||
"FINAL": {
|
||||
"DESCRIPTION": "Gracias por instalar Talk! Te enviamos un email para verificar tu identidad. Mientras se termina de configurar la cuenta, ya puedes empezar a interactuar con tus lectores",
|
||||
"LAUNCH": "Lanzar Talk",
|
||||
"CLOSE": "Cerrar este instalador"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,33 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {Layout} from '../components/ui/Layout';
|
||||
import {checkLogin, logout} from '../actions/auth';
|
||||
import Layout from '../components/ui/Layout';
|
||||
import {checkLogin, handleLogin, logout, requestPasswordReset} from '../actions/auth';
|
||||
import {FullLoading} from '../components/FullLoading';
|
||||
import {PermissionRequired} from '../components/PermissionRequired';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
|
||||
class LayoutContainer extends Component {
|
||||
componentWillMount () {
|
||||
const {checkLogin} = this.props;
|
||||
checkLogin().then(() => {
|
||||
if (!this.props.auth.isAdmin) {
|
||||
location.href = '/admin/login';
|
||||
}
|
||||
});
|
||||
checkLogin();
|
||||
}
|
||||
render () {
|
||||
const {isAdmin, loggedIn, loadingUser} = this.props.auth;
|
||||
const {
|
||||
isAdmin,
|
||||
loggedIn,
|
||||
loadingUser,
|
||||
loginError,
|
||||
passwordRequestSuccess
|
||||
} = this.props.auth;
|
||||
const {handleLogout} = this.props;
|
||||
if (loadingUser) { return <FullLoading />; }
|
||||
if (!isAdmin) { return <PermissionRequired />; }
|
||||
if (isAdmin && loggedIn) { return <Layout {...this.props} />; }
|
||||
if (!isAdmin) {
|
||||
return <AdminLogin
|
||||
handleLogin={this.props.handleLogin}
|
||||
requestPasswordReset={this.props.requestPasswordReset}
|
||||
passwordRequestSuccess={passwordRequestSuccess}
|
||||
errorMessage={loginError} />;
|
||||
}
|
||||
if (isAdmin && loggedIn) { return <Layout handleLogout={handleLogout} {...this.props} />; }
|
||||
return <FullLoading />;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +38,8 @@ const mapStateToProps = state => ({
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
checkLogin: () => dispatch(checkLogin()),
|
||||
handleLogin: (username, password) => dispatch(handleLogin(username, password)),
|
||||
requestPasswordReset: email => dispatch(requestPasswordReset(email)),
|
||||
handleLogout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
|
||||
@@ -17,22 +17,50 @@ import ModerationQueue from './ModerationQueue';
|
||||
import ModerationMenu from './components/ModerationMenu';
|
||||
import ModerationHeader from './components/ModerationHeader';
|
||||
import NotFoundAsset from './components/NotFoundAsset';
|
||||
import ModerationKeysModal from '../../components/ModerationKeysModal';
|
||||
|
||||
class ModerationContainer extends Component {
|
||||
state = {
|
||||
selectedIndex: 0
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const {toggleModal, singleView} = this.props;
|
||||
const {selectedIndex} = this.state;
|
||||
|
||||
this.props.fetchSettings();
|
||||
|
||||
key('s', () => singleView());
|
||||
key('shift+/', () => toggleModal(true));
|
||||
key('esc', () => toggleModal(false));
|
||||
key('j', () => this.setState({selectedIndex: selectedIndex + 1}));
|
||||
key('k', () => this.setState({selectedIndex: selectedIndex > 0 ? selectedIndex + 1 : selectedIndex}));
|
||||
key('r', () => this.moderate(false));
|
||||
key('t', () => this.moderate(true));
|
||||
}
|
||||
|
||||
moderate = (accept) => {
|
||||
const {data, route, acceptComment, rejectComment} = this.props;
|
||||
const {selectedIndex} = this.state;
|
||||
const activeTab = route.path === ':id' ? 'premod' : route.path;
|
||||
const comments = data[activeTab];
|
||||
const commentId = {commentId: comments[selectedIndex].id};
|
||||
|
||||
if (accept) {
|
||||
acceptComment(commentId);
|
||||
} else {
|
||||
rejectComment(commentId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
key.unbind('s');
|
||||
key.unbind('shift+/');
|
||||
key.unbind('esc');
|
||||
key.unbind('j');
|
||||
key.unbind('k');
|
||||
key.unbind('r');
|
||||
key.unbind('t');
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
@@ -43,7 +71,7 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {data, moderation, settings, assets, ...props} = this.props;
|
||||
const {data, moderation, settings, assets, modQueueResort, onClose, ...props} = this.props;
|
||||
const providedAssetId = this.props.params.id;
|
||||
const activeTab = this.props.route.path === ':id' ? 'premod' : this.props.route.path;
|
||||
|
||||
@@ -65,6 +93,8 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const comments = data[activeTab];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ModerationHeader asset={asset} />
|
||||
@@ -73,11 +103,14 @@ class ModerationContainer extends Component {
|
||||
premodCount={data.premodCount}
|
||||
rejectedCount={data.rejectedCount}
|
||||
flaggedCount={data.flaggedCount}
|
||||
modQueueResort={modQueueResort}
|
||||
/>
|
||||
<ModerationQueue
|
||||
data={data}
|
||||
currentAsset={asset}
|
||||
comments={comments}
|
||||
activeTab={activeTab}
|
||||
singleView={moderation.singleView}
|
||||
selectedIndex={this.state.selectedIndex}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
@@ -89,6 +122,9 @@ class ModerationContainer extends Component {
|
||||
handleClose={props.hideBanUserDialog}
|
||||
handleBanUser={props.banUser}
|
||||
/>
|
||||
<ModerationKeysModal
|
||||
open={moderation.modalOpen}
|
||||
onClose={onClose}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
|
||||
import Comment from './components/Comment';
|
||||
import styles from './components/styles.css';
|
||||
import EmptyCard from '../../components/EmptyCard';
|
||||
import {actionsMap} from './helpers/moderationQueueActionsMap';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const ModerationQueue = ({activeTab = 'premod', ...props}) => {
|
||||
const areComments = props.data[activeTab].length;
|
||||
const ModerationQueue = ({comments, selectedIndex, singleView, ...props}) => {
|
||||
return (
|
||||
<div id="moderationList">
|
||||
<div id="moderationList" className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
|
||||
<ul style={{paddingLeft: 0}}>
|
||||
{
|
||||
areComments
|
||||
? props.data[activeTab].map((comment, i) => {
|
||||
comments.length
|
||||
? comments.map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
return <Comment
|
||||
key={i}
|
||||
index={i}
|
||||
comment={comment}
|
||||
commentType={props.activeTab}
|
||||
selected={i === selectedIndex}
|
||||
suspectWords={props.suspectWords}
|
||||
actions={actionsMap[status]}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
@@ -37,12 +38,12 @@ const ModerationQueue = ({activeTab = 'premod', ...props}) => {
|
||||
};
|
||||
|
||||
ModerationQueue.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
comments: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default ModerationQueue;
|
||||
|
||||
@@ -6,8 +6,10 @@ import {Link} from 'react-router';
|
||||
|
||||
import styles from './styles.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
import ActionButton from '../../../components/ActionButton';
|
||||
import FlagBox from './FlagBox';
|
||||
import CommentType from './CommentType';
|
||||
import ActionButton from 'coral-admin/src/components/ActionButton';
|
||||
import BanUserButton from 'coral-admin/src/components/BanUserButton';
|
||||
|
||||
const linkify = new Linkify();
|
||||
|
||||
@@ -19,48 +21,52 @@ const Comment = ({actions = [], ...props}) => {
|
||||
const links = linkify.getMatches(props.comment.body);
|
||||
const actionSummaries = props.comment.action_summaries;
|
||||
return (
|
||||
<li tabIndex={props.index}
|
||||
className={`mdl-card mdl-shadow--2dp ${styles.Comment} ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<span>{props.comment.user.name}</span>
|
||||
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<span>
|
||||
{props.comment.user.name}
|
||||
</span>
|
||||
<span className={styles.created}>
|
||||
{timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
|
||||
</span>
|
||||
{props.comment.action_summaries ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
|
||||
<BanUserButton user={props.comment.user} onClick={() => props.showBanUserDialog(props.comment.user, props.comment.id)} />
|
||||
<CommentType type={props.commentType} />
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{actions.map((action, i) =>
|
||||
<ActionButton key={i}
|
||||
type={action}
|
||||
user={props.comment.user}
|
||||
acceptComment={() => props.acceptComment({commentId: props.comment.id})}
|
||||
rejectComment={() => props.rejectComment({commentId: props.comment.id})}
|
||||
showBanUserDialog={() => props.showBanUserDialog(props.comment.user, props.comment.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{props.comment.user.status === 'banned' ?
|
||||
<span className={styles.banned}>
|
||||
<Icon name='error_outline'/>
|
||||
{lang.t('comment.banned_user')}
|
||||
</span>
|
||||
: null}
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
{!props.currentAsset && (
|
||||
<div className={styles.moderateArticle}>
|
||||
Article: {props.comment.asset.title} <Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate Article</Link>
|
||||
Story: {props.comment.asset.title}
|
||||
{!props.currentAsset && (
|
||||
<Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate →</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.itemBody}>
|
||||
<p className={styles.body}>
|
||||
<Linkify component='span' properties={{style: linkStyles}}>
|
||||
<Highlighter searchWords={props.suspectWords} textToHighlight={props.comment.body}/>
|
||||
</Linkify>
|
||||
</p>
|
||||
<div className={styles.sideActions}>
|
||||
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{actions.map((action, i) =>
|
||||
<ActionButton key={i}
|
||||
type={action}
|
||||
user={props.comment.user}
|
||||
acceptComment={() => props.acceptComment({commentId: props.comment.id})}
|
||||
rejectComment={() => props.rejectComment({commentId: props.comment.id})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.itemBody}>
|
||||
<p className={styles.body}>
|
||||
<Linkify component='span' properties={{style: linkStyles}}>
|
||||
<Highlighter searchWords={props.suspectWords} textToHighlight={props.comment.body}/>
|
||||
</Linkify>
|
||||
</p>
|
||||
</div>
|
||||
{actionSummaries && <FlagBox actionSummaries={actionSummaries} />}
|
||||
</li>
|
||||
@@ -72,7 +78,6 @@ Comment.propTypes = {
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
comment: PropTypes.shape({
|
||||
body: PropTypes.string.isRequired,
|
||||
action_summaries: PropTypes.array,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
.commentType {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 11px;
|
||||
color: white;
|
||||
background: grey;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
line-height: 29px;
|
||||
padding: 2px 8px 2px 26px;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.premod {
|
||||
background: #063B9A;
|
||||
}
|
||||
|
||||
&.flagged {
|
||||
background: #d03235;
|
||||
}
|
||||
|
||||
&.no-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './CommentType.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
const CommentType = props => {
|
||||
const typeData = getTypeData(props.type);
|
||||
|
||||
return (
|
||||
<span className={`${styles.commentType} ${styles[typeData.className]}`}>
|
||||
<Icon name={typeData.icon}/>{typeData.text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const getTypeData = type => {
|
||||
switch (type) {
|
||||
case 'premod':
|
||||
return {icon: 'query_builder', text: 'Pre-Mod', className: 'premod'};
|
||||
case 'flagged':
|
||||
return {icon: 'flag', text: 'Flagged', className: 'flagged'};
|
||||
default:
|
||||
return {icon: 'flag', className: 'no-type'};
|
||||
}
|
||||
};
|
||||
|
||||
CommentType.propTypes = {
|
||||
type: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default CommentType;
|
||||
@@ -0,0 +1,55 @@
|
||||
.flagBox {
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding: 0 20px 16px;
|
||||
ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
.moreDetail {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-right: 10px;
|
||||
margin-top: 8px;
|
||||
color: black;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
i {
|
||||
vertical-align: middle;
|
||||
font-size: 12px;
|
||||
}
|
||||
ul {
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
margin-left: 7px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,47 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './styles.css';
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './FlagBox.css';
|
||||
|
||||
const FlagBox = props => (
|
||||
<div className={styles.flagBox}>
|
||||
<h3>Flags:</h3>
|
||||
<ul>
|
||||
{props.actionSummaries.map((action, i) =>
|
||||
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
class FlagBox extends Component {
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
showDetail: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleDetail = () => {
|
||||
this.setState((state) => ({
|
||||
showDetail: !state.showDetail
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
return (
|
||||
<div className={styles.flagBox}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<Icon name='flag'/><h3>Flags ({props.actionSummaries.length}):</h3>
|
||||
<ul>
|
||||
{props.actionSummaries.map((action, i) =>
|
||||
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
|
||||
)}
|
||||
</ul>
|
||||
{/* <a onClick={this.toggleDetail} className={styles.moreDetail}>More detail</a>*/}
|
||||
</div>
|
||||
{this.state.showDetail && (<div className={styles.detail}>
|
||||
<ul>
|
||||
{props.actionSummaries.map((action, i) =>
|
||||
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FlagBox.propTypes = {
|
||||
actionSummaries: PropTypes.array.isRequired
|
||||
|
||||
@@ -1,42 +1,64 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React, {PropTypes, Component} from 'react';
|
||||
import CommentCount from './CommentCount';
|
||||
import styles from './styles.css';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const ModerationMenu = ({asset, premodCount, rejectedCount, flaggedCount}) => {
|
||||
const premodPath = asset ? `/admin/moderate/premod/${asset.id}` : '/admin/moderate/premod';
|
||||
const rejectPath = asset ? `/admin/moderate/rejected/${asset.id}` : '/admin/moderate/rejected';
|
||||
const flagPath = asset ? `/admin/moderate/flagged/${asset.id}` : '/admin/moderate/flagged';
|
||||
return (
|
||||
<div className='mdl-tabs'>
|
||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||
<div>
|
||||
<Link to={premodPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{lang.t('modqueue.premod')} <CommentCount count={premodCount} />
|
||||
</Link>
|
||||
<Link to={rejectPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{lang.t('modqueue.rejected')} <CommentCount count={rejectedCount} />
|
||||
</Link>
|
||||
<Link to={flagPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{lang.t('modqueue.flagged')} <CommentCount count={flaggedCount} />
|
||||
</Link>
|
||||
class ModerationMenu extends Component {
|
||||
state = {
|
||||
sort: 'REVERSE_CHRONOLOGICAL',
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
premodCount: PropTypes.number.isRequired,
|
||||
rejectedCount: PropTypes.number.isRequired,
|
||||
flaggedCount: PropTypes.number.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
id: PropTypes.string
|
||||
})
|
||||
}
|
||||
|
||||
selectSort = (sort) => {
|
||||
this.setState({sort});
|
||||
this.props.modQueueResort(sort);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {asset, premodCount, rejectedCount, flaggedCount} = this.props;
|
||||
const premodPath = asset ? `/admin/moderate/premod/${asset.id}` : '/admin/moderate/premod';
|
||||
const rejectPath = asset ? `/admin/moderate/rejected/${asset.id}` : '/admin/moderate/rejected';
|
||||
const flagPath = asset ? `/admin/moderate/flagged/${asset.id}` : '/admin/moderate/flagged';
|
||||
return (
|
||||
<div className='mdl-tabs'>
|
||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||
<div className={styles.tabBarPadding}/>
|
||||
<div>
|
||||
<Link to={premodPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{lang.t('modqueue.premod')} <CommentCount count={premodCount} />
|
||||
</Link>
|
||||
<Link to={rejectPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{lang.t('modqueue.rejected')} <CommentCount count={rejectedCount} />
|
||||
</Link>
|
||||
<Link to={flagPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{lang.t('modqueue.flagged')} <CommentCount count={flaggedCount} />
|
||||
</Link>
|
||||
</div>
|
||||
<SelectField
|
||||
className={styles.selectField}
|
||||
label='Sort'
|
||||
value={this.state.sort}
|
||||
onChange={sort => this.selectSort(sort)}>
|
||||
<Option value={'REVERSE_CHRONOLOGICAL'}>Newest First</Option>
|
||||
<Option value={'CHRONOLOGICAL'}>Oldest First</Option>
|
||||
</SelectField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ModerationMenu.propTypes = {
|
||||
premodCount: PropTypes.number.isRequired,
|
||||
rejectedCount: PropTypes.number.isRequired,
|
||||
flaggedCount: PropTypes.number.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
id: PropTypes.string
|
||||
})
|
||||
};
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModerationMenu;
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
.tabBar {
|
||||
background-color: rgba(44, 44, 44, 0.89);
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tabBarPadding {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
@@ -130,7 +136,7 @@ span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.singleView .listItem.activeItem {
|
||||
&.singleView .listItem.selected {
|
||||
display: block;
|
||||
height: 100%;
|
||||
font-size: 1.5em;
|
||||
@@ -141,7 +147,6 @@ span {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
@@ -156,16 +161,20 @@ span {
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 14px;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms;
|
||||
margin-top: 0;
|
||||
transition: all 200ms;
|
||||
padding: 10px 0 0;
|
||||
min-height: 220px;
|
||||
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
@@ -175,6 +184,11 @@ span {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
max-width: 670px;
|
||||
max-height: 410px;
|
||||
}
|
||||
|
||||
.context {
|
||||
a {
|
||||
color: #f36451;
|
||||
@@ -184,11 +198,8 @@ span {
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -198,6 +209,7 @@ span {
|
||||
justify-content: space-between;
|
||||
|
||||
.author {
|
||||
font-weight: 600;
|
||||
min-width: 230px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -207,6 +219,9 @@ span {
|
||||
.itemBody {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@@ -222,7 +237,8 @@ span {
|
||||
.created {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-left: 40px;
|
||||
margin-left: 15px;
|
||||
line-height: 1px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
@@ -233,10 +249,10 @@ span {
|
||||
.body {
|
||||
margin-top: 0px;
|
||||
flex: 1;
|
||||
font-size: 0.88em;
|
||||
color: black;
|
||||
max-width: 500px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
@@ -304,20 +320,29 @@ span {
|
||||
}
|
||||
|
||||
.Comment {
|
||||
|
||||
.moderateArticle {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
margin: 10px 0;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
max-width: 500px;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
color: #679af3;
|
||||
color: #063b9a;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
font-weight: 400;
|
||||
font-weight: 500;
|
||||
letter-spacing: .5px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
margin-left: 5px;
|
||||
padding-bottom: 0px;
|
||||
border-bottom: solid 1px;
|
||||
line-height: 16px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -325,12 +350,40 @@ span {
|
||||
}
|
||||
}
|
||||
|
||||
.flagBox {
|
||||
max-width: 480px;
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
.selectField {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 36px;
|
||||
top: 5px;
|
||||
margin-right: 10px;
|
||||
background: #FFF;
|
||||
padding: 10px 15px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
|
||||
> div {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.7px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
label {
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -1,13 +1,14 @@
|
||||
export const actionsMap = {
|
||||
PREMOD: ['REJECT', 'APPROVE', 'BAN'],
|
||||
FLAGGED: ['REJECT', 'APPROVE', 'BAN'],
|
||||
REJECTED: ['APPROVE']
|
||||
PREMOD: ['APPROVE', 'REJECT'],
|
||||
FLAGGED: ['APPROVE', 'REJECT'],
|
||||
REJECTED: ['APPROVE', 'REJECTED']
|
||||
};
|
||||
|
||||
export const menuActionsMap = {
|
||||
'REJECT': {status: 'REJECTED', icon: 'close', key: 'r'},
|
||||
'APPROVE': {status: 'ACCEPTED', icon: 'done', key: 't'},
|
||||
'FLAGGED': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
|
||||
'BAN': {status: 'BANNED', icon: 'not interested'},
|
||||
'REJECT': {status: 'REJECTED', text: 'Reject', icon: 'close', key: 'r'},
|
||||
'REJECTED': {status: 'REJECTED', text: 'Rejected', icon: 'close'},
|
||||
'APPROVE': {status: 'ACCEPTED', text: 'Approve', icon: 'done', key: 't'},
|
||||
'FLAGGED': {status: 'FLAGGED', text: 'Flag', icon: 'flag', filter: 'Untouched'},
|
||||
'BAN': {status: 'BANNED', text: 'Ban User', icon: 'not interested'},
|
||||
'': {icon: 'done'}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
fragment metrics on Asset {
|
||||
id
|
||||
title
|
||||
url
|
||||
author
|
||||
created_at
|
||||
action_summaries {
|
||||
type: __typename
|
||||
actionCount
|
||||
actionableItemCount
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,45 @@
|
||||
import {graphql} from 'react-apollo';
|
||||
|
||||
import MOST_FLAGS from './mostFlags.graphql';
|
||||
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
|
||||
import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
|
||||
|
||||
export const mostFlags = graphql(MOST_FLAGS, {
|
||||
options: () => {
|
||||
|
||||
// currently hard-coded per Greg's advice
|
||||
const fiveMinutesAgo = new Date();
|
||||
fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 305);
|
||||
return {
|
||||
variables: {
|
||||
sort: 'FLAG',
|
||||
from: fiveMinutesAgo.toISOString(),
|
||||
to: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
import METRICS from './metricsQuery.graphql';
|
||||
|
||||
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
|
||||
options: ({params: {id = null}}) => {
|
||||
return {
|
||||
variables: {
|
||||
asset_id: id
|
||||
asset_id: id,
|
||||
sort: 'REVERSE_CHRONOLOGICAL'
|
||||
}
|
||||
};
|
||||
},
|
||||
props: ({ownProps: {params: {id = null}}, data}) => ({
|
||||
data,
|
||||
modQueueResort: modQueueResort(id, data.fetchMore)
|
||||
})
|
||||
});
|
||||
|
||||
export const getMetrics = graphql(METRICS, {
|
||||
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
|
||||
|
||||
return {
|
||||
variables: {
|
||||
from: dashboardWindowStart,
|
||||
to: dashboardWindowEnd
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const modUserFlaggedQuery = graphql(MOD_USER_FLAGGED_QUERY);
|
||||
|
||||
export const modQueueResort = (id, fetchMore) => (sort) => {
|
||||
return fetchMore({
|
||||
query: MOD_QUEUE_QUERY,
|
||||
variables: {
|
||||
asset_id: id,
|
||||
sort
|
||||
},
|
||||
updateQuery: (oldData, {fetchMoreResult:{data}}) => data
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#import "../fragments/assetMetricsView.graphql"
|
||||
|
||||
query Metrics ($from: Date!, $to: Date!) {
|
||||
assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
|
||||
...metrics
|
||||
}
|
||||
assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) {
|
||||
...metrics
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
query ModQueue ($asset_id: ID) {
|
||||
query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
|
||||
premod: comments(query: {
|
||||
statuses: [PREMOD],
|
||||
asset_id: $asset_id
|
||||
asset_id: $asset_id,
|
||||
sort: $sort
|
||||
}) {
|
||||
...commentView
|
||||
}
|
||||
flagged: comments(query: {
|
||||
action_type: FLAG,
|
||||
asset_id: $asset_id,
|
||||
statuses: [NONE, PREMOD]
|
||||
statuses: [NONE, PREMOD],
|
||||
sort: $sort
|
||||
}) {
|
||||
...commentView
|
||||
action_summaries {
|
||||
@@ -22,7 +24,8 @@ query ModQueue ($asset_id: ID) {
|
||||
}
|
||||
rejected: comments(query: {
|
||||
statuses: [REJECTED],
|
||||
asset_id: $asset_id
|
||||
asset_id: $asset_id,
|
||||
sort: $sort
|
||||
}) {
|
||||
...commentView
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
query Metrics ($from: Date!, $to: Date!, $sort: ACTION_TYPE!) {
|
||||
metrics(from: $from, to: $to, sort: $sort) {
|
||||
id
|
||||
title
|
||||
url
|
||||
commentCount
|
||||
author
|
||||
created_at
|
||||
action_summaries {
|
||||
actionCount
|
||||
actionableItemCount
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import * as actions from '../constants/auth';
|
||||
const initialState = Map({
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
isAdmin: false
|
||||
isAdmin: false,
|
||||
loginError: null,
|
||||
passwordRequestSuccess: null
|
||||
});
|
||||
|
||||
export default function auth (state = initialState, action) {
|
||||
@@ -25,6 +27,14 @@ export default function auth (state = initialState, action) {
|
||||
.set('user', action.user);
|
||||
case actions.LOGOUT_SUCCESS:
|
||||
return initialState;
|
||||
case actions.LOGIN_REQUEST:
|
||||
return state.set('loginError', null);
|
||||
case actions.LOGIN_FAILURE:
|
||||
return state.set('loginError', action.message);
|
||||
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
|
||||
return state.set('passwordRequestSuccess', null);
|
||||
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
|
||||
return state.set('passwordRequestSuccess', 'If you have a registered account, a password reset link was sent to that email.');
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Map} from 'immutable';
|
||||
import {Map, List} from 'immutable';
|
||||
|
||||
import * as actions from '../constants/install';
|
||||
|
||||
@@ -6,7 +6,10 @@ const initialState = Map({
|
||||
isLoading: false,
|
||||
data: Map({
|
||||
settings: Map({
|
||||
organizationName: ''
|
||||
organizationName: '',
|
||||
domains: Map({
|
||||
whitelist: List()
|
||||
})
|
||||
}),
|
||||
user: Map({
|
||||
username: '',
|
||||
@@ -33,6 +36,10 @@ const initialState = Map({
|
||||
{
|
||||
text: '2. Create your account',
|
||||
step: 2
|
||||
},
|
||||
{
|
||||
text: '3. Domain Whitelist',
|
||||
step: 3
|
||||
}],
|
||||
installRequest: null,
|
||||
installRequestError: null,
|
||||
@@ -50,6 +57,9 @@ export default function install (state = initialState, action) {
|
||||
case actions.GO_TO_STEP:
|
||||
return state
|
||||
.set('step', action.step);
|
||||
case actions.UPDATE_PERMITTED_DOMAINS_SETTINGS:
|
||||
return state
|
||||
.setIn(['data', 'settings', 'domains', 'whitelist'], action.value);
|
||||
case actions.UPDATE_FORMDATA_SETTINGS:
|
||||
return state
|
||||
.setIn(['data', 'settings', action.name], action.value);
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import {Map, List} from 'immutable';
|
||||
import * as actions from '../actions/settings';
|
||||
|
||||
// this is initialized here because
|
||||
// currently you have to reload the dashboard to get new stats
|
||||
// cleaner updates are planned in the future.
|
||||
// TODO: if there are more than two fields for the dashboard being created here,
|
||||
// please create a new reducer specifically for the Dashboard.
|
||||
const DASHBOARD_WINDOW_MINUTES = 5;
|
||||
let then = new Date();
|
||||
then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES);
|
||||
|
||||
const initialState = Map({
|
||||
wordlist: Map({
|
||||
banned: List(),
|
||||
suspect: List()
|
||||
}),
|
||||
dashboardWindowStart: then.toISOString(),
|
||||
dashboardWindowEnd: new Date().toISOString(),
|
||||
domains: Map({
|
||||
whitelist: List()
|
||||
}),
|
||||
@@ -22,7 +33,7 @@ export default function settings (state = initialState, action) {
|
||||
.set('fetchSettingsError', null);
|
||||
case actions.SETTINGS_RECEIVED:
|
||||
return state.merge({
|
||||
fetchingSettings: null,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
});
|
||||
@@ -32,7 +43,7 @@ export default function settings (state = initialState, action) {
|
||||
.set('fetchSettingsError', action.error);
|
||||
case actions.SETTINGS_UPDATED:
|
||||
return state.merge({
|
||||
fetchingSettings: null,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import getNetworkInterface from './transport';
|
||||
|
||||
export const client = new ApolloClient({
|
||||
addTypename: true,
|
||||
queryTransformer: addTypename,
|
||||
dataIdFromObject: (result) => {
|
||||
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"en": {
|
||||
"errors": {
|
||||
"NOT_AUTHORIZED": "Your username or password is not recognized by our system."
|
||||
},
|
||||
"community": {
|
||||
"username_and_email": "Username and Email",
|
||||
"account_creation_date": "Account Creation Date",
|
||||
@@ -63,6 +66,9 @@
|
||||
"copy": "Copy to Clipboard"
|
||||
},
|
||||
"configure": {
|
||||
"stream-settings": "Stream Settings",
|
||||
"moderation-settings": "Moderation Settings",
|
||||
"tech-settings": "Tech Settings",
|
||||
"custom-css-url": "Custom CSS URL",
|
||||
"custom-css-url-desc": "URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external.",
|
||||
"dashboard": "Dashboard",
|
||||
@@ -75,8 +81,6 @@
|
||||
"include-text": "Include your text here.",
|
||||
"comment-settings": "Settings",
|
||||
"embed-comment-stream": "Embed Stream",
|
||||
"banned-word-header": "Write the banned words list",
|
||||
"suspect-word-header": "Write the suspect words list",
|
||||
"banned-word-text": "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated 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.",
|
||||
"wordlist": "Banned Words",
|
||||
@@ -98,8 +102,8 @@
|
||||
"comment-count-text-pre": "Comments will be limited to ",
|
||||
"comment-count-text-post": " characters.",
|
||||
"comment-count-error": "Please enter a valid number.",
|
||||
"domain-list-title": "Domain Whitelist",
|
||||
"domain-list-text": "Some instructions on how to type the urls."
|
||||
"domain-list-title": "Permitted Domains",
|
||||
"domain-list-text": "Enter the domains you would like to permit for Talk, e.g. your local, staging and production environments (ex. localhost:3000, staging.domain.com, domain.com)."
|
||||
},
|
||||
"bandialog": {
|
||||
"ban_user": "Ban User?",
|
||||
@@ -125,7 +129,9 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"no_flags": "There have been no flags in the last 5 minutes! Hooray!",
|
||||
"comment_count": "Comments"
|
||||
"no_likes": "There have been no likes in the last 5 minutes. All quiet.",
|
||||
"flags": "Flags",
|
||||
"comment_count": "comments"
|
||||
},
|
||||
"streams": {
|
||||
"empty_result": "No assets match this search. Maybe try widening your search?",
|
||||
@@ -146,6 +152,9 @@
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"errors": {
|
||||
"NOT_AUTHORIZED": "Acción no autorizada."
|
||||
},
|
||||
"community": {
|
||||
"username_and_email": "Usuario y E-mail",
|
||||
"account_creation_date": "Fecha de creación de la cuenta",
|
||||
@@ -210,6 +219,9 @@
|
||||
"username_flags": ""
|
||||
},
|
||||
"configure": {
|
||||
"stream-settings": "Configuración de Comentarios",
|
||||
"moderation-settings": "Configuración de Moderación",
|
||||
"tech-settings": "Configuración Technical",
|
||||
"custom-css-url": "URL CSS a medida",
|
||||
"custom-css-url-desc": "URL de una hoja de estilo que va a sobrescribir los estilos por defecto de Embed Stream. Puede ser interna o externa.",
|
||||
"dashboard": "Panel",
|
||||
@@ -223,8 +235,6 @@
|
||||
"comment-settings": "Configuración de Comentarios",
|
||||
"embed-comment-stream": "Colocar Hilo de Comentarios",
|
||||
"wordlist": "Palabras Suspendidas y Suspechosas",
|
||||
"banned-word-header": "Escribir las palabras no permitidas",
|
||||
"suspect-word-header": "Write the suspect words list",
|
||||
"banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente separadas de los comentarios publicados.",
|
||||
"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.",
|
||||
"banned-words-title": "Banned words list",
|
||||
@@ -247,21 +257,23 @@
|
||||
"comment-count-text-post": " caracteres",
|
||||
"comment-count-error": "Por favor escribe un número válido.",
|
||||
"domain-list-title": "Lista de Dominios Permitidos",
|
||||
"domain-list-text": "Instrucciones de como ingresar las URLs."
|
||||
"domain-list-text": "Agrega dominios permitidos a Talk, e.g. tu localhost, staging y ambientes de production (ex. localhost:3000, staging.domain.com, domain.com)."
|
||||
},
|
||||
"embedlink": {
|
||||
"copy": "Copiar"
|
||||
},
|
||||
"bandialog": {
|
||||
"ban_user": "Quieres suspender el Usuario?",
|
||||
"are_you_sure": "Estas segura que quieres suspender a {props.author.username}?",
|
||||
"are_you_sure": "Estas segura que quieres suspender a {0}?",
|
||||
"note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.",
|
||||
"cancel": "Cancelar",
|
||||
"yes_ban_user": "Si, Suspendan el usuario"
|
||||
},
|
||||
"dashbord": {
|
||||
"no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!",
|
||||
"comment_count": "Comentarios"
|
||||
"no_likes": "A nadie le ha gustado algún comentario en los últimos 5 minutos. Todo tranquilo.",
|
||||
"flags": "Marcados",
|
||||
"comment_count": "comentarios"
|
||||
},
|
||||
"streams": {
|
||||
"empty_result": "No se encuentro articulo con esta busqueda. Tal vez extender la busqueda?",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.Reply {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Comment {
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import PubDate from 'coral-plugin-pubdate/PubDate';
|
||||
import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
|
||||
import FlagComment from 'coral-plugin-flags/FlagComment';
|
||||
import LikeButton from 'coral-plugin-likes/LikeButton';
|
||||
import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton';
|
||||
import LoadMore from 'coral-embed-stream/src/LoadMore';
|
||||
|
||||
import styles from './Comment.css';
|
||||
@@ -25,6 +26,11 @@ const getActionSummary = (type, comment) => comment.action_summaries
|
||||
.filter((a) => a.__typename === type)[0];
|
||||
const isStaff = (tags) => !tags.every((t) => t.name !== 'STAFF') ;
|
||||
|
||||
// hold actions links (e.g. Like, Reply) along the comment footer
|
||||
const ActionButton = ({children}) => {
|
||||
return <span className="comment__action-button comment__action-button--nowrap">{ children }</span>;
|
||||
};
|
||||
|
||||
class Comment extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
@@ -38,12 +44,12 @@ class Comment extends React.Component {
|
||||
// id of currently opened ReplyBox. tracked in Stream.js
|
||||
activeReplyBox: PropTypes.string.isRequired,
|
||||
setActiveReplyBox: PropTypes.func.isRequired,
|
||||
refetch: PropTypes.func.isRequired,
|
||||
showSignInDialog: PropTypes.func.isRequired,
|
||||
postFlag: PropTypes.func.isRequired,
|
||||
postLike: PropTypes.func.isRequired,
|
||||
deleteAction: PropTypes.func.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
highlighted: PropTypes.string,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
@@ -74,7 +80,13 @@ class Comment extends React.Component {
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
}).isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
|
||||
// dispatch action to add a tag to a comment
|
||||
addCommentTag: React.PropTypes.func,
|
||||
|
||||
// dispatch action to remove a tag from a comment
|
||||
removeCommentTag: React.PropTypes.func,
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -85,60 +97,105 @@ class Comment extends React.Component {
|
||||
asset,
|
||||
depth,
|
||||
postItem,
|
||||
refetch,
|
||||
addNotification,
|
||||
showSignInDialog,
|
||||
postLike,
|
||||
highlighted,
|
||||
postFlag,
|
||||
postDontAgree,
|
||||
loadMore,
|
||||
setActiveReplyBox,
|
||||
activeReplyBox,
|
||||
deleteAction
|
||||
deleteAction,
|
||||
addCommentTag,
|
||||
removeCommentTag,
|
||||
} = this.props;
|
||||
|
||||
const like = getActionSummary('LikeActionSummary', comment);
|
||||
const flag = getActionSummary('FlagActionSummary', comment);
|
||||
const dontagree = getActionSummary('DontAgreeActionSummary', comment);
|
||||
let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`;
|
||||
commentClass += highlighted === comment.id ? ' highlighted-comment' : '';
|
||||
|
||||
// call a function, and if it errors, call addNotification('error', ...) (e.g. to show user a snackbar)
|
||||
const notifyOnError = (fn, errorToMessage) => async () => {
|
||||
if (typeof errorToMessage !== 'function') {errorToMessage = (error) => error.message;}
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
addNotification('error', errorToMessage(error));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const addBestTag = notifyOnError(() => addCommentTag({
|
||||
id: comment.id,
|
||||
tag: BEST_TAG,
|
||||
}), () => 'Failed to tag comment as best');
|
||||
|
||||
const removeBestTag = notifyOnError(() => removeCommentTag({
|
||||
id: comment.id,
|
||||
tag: BEST_TAG,
|
||||
}), () => 'Failed to remove best comment tag');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`}
|
||||
className={commentClass}
|
||||
id={`c_${comment.id}`}
|
||||
style={{marginLeft: depth * 30}}>
|
||||
<hr aria-hidden={true} />
|
||||
<AuthorName
|
||||
author={comment.user}/>
|
||||
{ isStaff(comment.tags)
|
||||
? <TagLabel isStaff={true}/>
|
||||
? <TagLabel>Staff</TagLabel>
|
||||
: null }
|
||||
|
||||
{ commentIsBest(comment)
|
||||
? <TagLabel><BestIndicator /></TagLabel>
|
||||
: null }
|
||||
<PubDate created_at={comment.created_at} />
|
||||
<Content body={comment.body} />
|
||||
<div className="commentActionsLeft">
|
||||
<LikeButton
|
||||
like={like}
|
||||
id={comment.id}
|
||||
postLike={postLike}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
<ReplyButton
|
||||
onClick={() => setActiveReplyBox(comment.id)}
|
||||
parentCommentId={parentId || comment.id}
|
||||
currentUserId={currentUser && currentUser.id}
|
||||
banned={false} />
|
||||
<div className="commentActionsLeft comment__action-container">
|
||||
<ActionButton>
|
||||
<LikeButton
|
||||
like={like}
|
||||
id={comment.id}
|
||||
postLike={postLike}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
</ActionButton>
|
||||
<ActionButton>
|
||||
<ReplyButton
|
||||
onClick={() => setActiveReplyBox(comment.id)}
|
||||
parentCommentId={parentId || comment.id}
|
||||
currentUserId={currentUser && currentUser.id}
|
||||
banned={false} />
|
||||
</ActionButton>
|
||||
<ActionButton>
|
||||
<IfUserCanModifyBest user={currentUser}>
|
||||
<BestButton
|
||||
isBest={commentIsBest(comment)}
|
||||
addBest={addBestTag}
|
||||
removeBest={removeBestTag} />
|
||||
</IfUserCanModifyBest>
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="commentActionsRight">
|
||||
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
|
||||
<FlagComment
|
||||
flag={flag && flag.current_user ? flag : dontagree}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
<div className="commentActionsRight comment__action-container">
|
||||
<ActionButton>
|
||||
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
|
||||
</ActionButton>
|
||||
<ActionButton>
|
||||
<FlagComment
|
||||
flag={flag && flag.current_user ? flag : dontagree}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser} />
|
||||
</ActionButton>
|
||||
</div>
|
||||
{
|
||||
activeReplyBox === comment.id
|
||||
@@ -158,7 +215,6 @@ class Comment extends React.Component {
|
||||
comment.replies &&
|
||||
comment.replies.map(reply => {
|
||||
return <Comment
|
||||
refetch={refetch}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
@@ -166,10 +222,13 @@ class Comment extends React.Component {
|
||||
postItem={postItem}
|
||||
depth={depth + 1}
|
||||
asset={asset}
|
||||
highlighted={highlighted}
|
||||
currentUser={currentUser}
|
||||
postLike={postLike}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
addCommentTag={addCommentTag}
|
||||
removeCommentTag={removeCommentTag}
|
||||
showSignInDialog={showSignInDialog}
|
||||
reactKey={reply.id}
|
||||
key={reply.id}
|
||||
|
||||
@@ -13,7 +13,7 @@ const {addNotification, clearNotification} = notificationActions;
|
||||
const {fetchAssetSuccess} = assetActions;
|
||||
|
||||
import {queryStream} from 'coral-framework/graphql/queries';
|
||||
import {postComment, postFlag, postLike, postDontAgree, deleteAction} from 'coral-framework/graphql/mutations';
|
||||
import {postComment, postFlag, postLike, postDontAgree, deleteAction, addCommentTag, removeCommentTag} from 'coral-framework/graphql/mutations';
|
||||
import {editName} from 'coral-framework/actions/user';
|
||||
import {updateCountCache} from 'coral-framework/actions/asset';
|
||||
import {Notification, notificationActions, authActions, assetActions, pym} from 'coral-framework';
|
||||
@@ -31,12 +31,13 @@ import ChangeUsernameContainer from '../../coral-sign-in/containers/ChangeUserna
|
||||
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
|
||||
import RestrictedContent from 'coral-framework/components/RestrictedContent';
|
||||
import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer';
|
||||
import Comment from './Comment';
|
||||
import LoadMore from './LoadMore';
|
||||
import NewCount from './NewCount';
|
||||
|
||||
class Embed extends Component {
|
||||
|
||||
state = {activeTab: 0, showSignInDialog: false};
|
||||
state = {activeTab: 0, showSignInDialog: false, activeReplyBox: ''};
|
||||
|
||||
changeTab = (tab) => {
|
||||
|
||||
@@ -54,28 +55,17 @@ class Embed extends Component {
|
||||
data: React.PropTypes.shape({
|
||||
loading: React.PropTypes.bool,
|
||||
error: React.PropTypes.object
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
|
||||
// dispatch action to add a tag to a comment
|
||||
addCommentTag: React.PropTypes.func,
|
||||
|
||||
// dispatch action to remove a tag from a comment
|
||||
removeCommentTag: React.PropTypes.func,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
pym.sendMessage('childReady');
|
||||
|
||||
pym.onMessage('DOMContentLoaded', hash => {
|
||||
const commentId = hash.replace('#', 'c_');
|
||||
let count = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (document.getElementById(commentId)) {
|
||||
window.clearInterval(interval);
|
||||
pym.scrollParentToChildEl(commentId);
|
||||
}
|
||||
|
||||
if (++count > 100) { // ~10 seconds
|
||||
// give up waiting for the comments to load.
|
||||
// it would be weird for the page to jump after that long.
|
||||
window.clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
@@ -85,11 +75,29 @@ class Embed extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if(!isEqual(prevProps.data.comment, this.props.data.comment)) {
|
||||
|
||||
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
|
||||
setTimeout(()=>pym.scrollParentToChildEl(`c_${this.props.data.comment.id}`), 0);
|
||||
}
|
||||
}
|
||||
|
||||
setActiveReplyBox = (reactKey) => {
|
||||
if (!this.props.auth.user) {
|
||||
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
} else {
|
||||
this.setState({activeReplyBox: reactKey});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {activeTab} = this.state;
|
||||
const {closedAt, countCache = {}} = this.props.asset;
|
||||
const {loading, asset, refetch} = this.props.data;
|
||||
const {loading, asset, refetch, comment} = this.props.data;
|
||||
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
|
||||
const highlightedComment = comment && comment.parent ? comment.parent : comment;
|
||||
|
||||
const openStream = closedAt === null;
|
||||
|
||||
@@ -116,7 +124,7 @@ class Embed extends Component {
|
||||
<Tab>{lang.t('profile')}</Tab>
|
||||
<Tab restricted={!isAdmin}>Configure Stream</Tab>
|
||||
</TabBar>
|
||||
{loggedIn && <UserBox user={user} logout={this.props.logout} changeTab={this.changeTab}/>}
|
||||
{loggedIn && <UserBox user={user} logout={() => this.props.logout().then(refetch)} changeTab={this.changeTab}/>}
|
||||
<TabContent show={activeTab === 0}>
|
||||
{
|
||||
openStream
|
||||
@@ -157,8 +165,33 @@ class Embed extends Component {
|
||||
</div>
|
||||
: <p>{asset.settings.closedMessage}</p>
|
||||
}
|
||||
{!loggedIn && <SignInContainer requireEmailConfirmation={asset.settings.requireEmailConfirmation} offset={signInOffset}/>}
|
||||
{!loggedIn && <SignInContainer
|
||||
requireEmailConfirmation={asset.settings.requireEmailConfirmation}
|
||||
refetch={refetch}
|
||||
offset={signInOffset}/>}
|
||||
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} offset={signInOffset} user={user} />}
|
||||
{
|
||||
highlightedComment &&
|
||||
<Comment
|
||||
refetch={refetch}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
activeReplyBox={this.state.activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
depth={0}
|
||||
postItem={this.props.postItem}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
highlighted={comment.id}
|
||||
postLike={this.props.postLike}
|
||||
postFlag={this.props.postFlag}
|
||||
postDontAgree={this.props.postDontAgree}
|
||||
loadMore={this.props.loadMore}
|
||||
deleteAction={this.props.deleteAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
key={highlightedComment.id}
|
||||
reactKey={highlightedComment.id}
|
||||
comment={highlightedComment} />
|
||||
}
|
||||
<NewCount
|
||||
commentCount={asset.commentCount}
|
||||
countCache={countCache[asset.id]}
|
||||
@@ -167,21 +200,26 @@ class Embed extends Component {
|
||||
assetId={asset.id}
|
||||
updateCountCache={this.props.updateCountCache}
|
||||
/>
|
||||
<Stream
|
||||
refetch={refetch}
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postLike={this.props.postLike}
|
||||
postFlag={this.props.postFlag}
|
||||
postDontAgree={this.props.postDontAgree}
|
||||
getCounts={this.props.getCounts}
|
||||
updateCountCache={this.props.updateCountCache}
|
||||
loadMore={this.props.loadMore}
|
||||
deleteAction={this.props.deleteAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
comments={asset.comments} />
|
||||
<div className="embed__stream">
|
||||
<Stream
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
activeReplyBox={this.state.activeReplyBox}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postLike={this.props.postLike}
|
||||
postFlag={this.props.postFlag}
|
||||
postDontAgree={this.props.postDontAgree}
|
||||
getCounts={this.props.getCounts}
|
||||
addCommentTag={this.props.addCommentTag}
|
||||
removeCommentTag={this.props.removeCommentTag}
|
||||
updateCountCache={this.props.updateCountCache}
|
||||
loadMore={this.props.loadMore}
|
||||
deleteAction={this.props.deleteAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
comments={asset.comments} />
|
||||
</div>
|
||||
<Notification
|
||||
notifLength={4500}
|
||||
clearNotification={this.props.clearNotification}
|
||||
@@ -250,6 +288,8 @@ export default compose(
|
||||
postFlag,
|
||||
postLike,
|
||||
postDontAgree,
|
||||
addCommentTag,
|
||||
removeCommentTag,
|
||||
deleteAction,
|
||||
queryStream
|
||||
)(Embed);
|
||||
|
||||
@@ -19,12 +19,13 @@ const NewCount = (props) => {
|
||||
|
||||
return <div className='coral-new-comments'>
|
||||
{
|
||||
props.countCache && newComments > 0 &&
|
||||
props.countCache && newComments > 0 ?
|
||||
<button onClick={onLoadMoreClick(props)} className='coral-load-more'>
|
||||
{newComments === 1
|
||||
? lang.t('newCount', newComments, lang.t('comment'))
|
||||
: lang.t('newCount', newComments, lang.t('comments'))}
|
||||
</button>
|
||||
: null
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comment
|
||||
class Stream extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
refetch: PropTypes.func.isRequired,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
@@ -13,13 +12,18 @@ class Stream extends React.Component {
|
||||
currentUser: PropTypes.shape({
|
||||
username: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
})
|
||||
}),
|
||||
|
||||
// dispatch action to add a tag to a comment
|
||||
addCommentTag: React.PropTypes.func,
|
||||
|
||||
// dispatch action to remove a tag from a comment
|
||||
removeCommentTag: React.PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {activeReplyBox: '', countPoll: null};
|
||||
this.setActiveReplyBox = this.setActiveReplyBox.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -42,15 +46,6 @@ class Stream extends React.Component {
|
||||
clearInterval(this.state.countPoll);
|
||||
}
|
||||
|
||||
setActiveReplyBox (reactKey) {
|
||||
if (!this.props.currentUser) {
|
||||
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
} else {
|
||||
this.setState({activeReplyBox: reactKey});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
comments,
|
||||
@@ -64,17 +59,17 @@ class Stream extends React.Component {
|
||||
loadMore,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
refetch
|
||||
addCommentTag,
|
||||
removeCommentTag
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id='stream'>
|
||||
{
|
||||
comments.map(comment =>
|
||||
<Comment
|
||||
refetch={refetch}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
activeReplyBox={this.state.activeReplyBox}
|
||||
setActiveReplyBox={this.props.setActiveReplyBox}
|
||||
activeReplyBox={this.props.activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
depth={0}
|
||||
postItem={postItem}
|
||||
@@ -83,6 +78,8 @@ class Stream extends React.Component {
|
||||
postLike={postLike}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
addCommentTag={addCommentTag}
|
||||
removeCommentTag={removeCommentTag}
|
||||
loadMore={loadMore}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
|
||||
@@ -172,7 +172,7 @@ hr {
|
||||
|
||||
.coral-plugin-author-name-text {
|
||||
display: inline-block;
|
||||
margin: 10px 8px 10px 0;
|
||||
margin: 10px 5px 10px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -180,13 +180,18 @@ hr {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.highlighted-comment {
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid rgb(35,118,216);
|
||||
}
|
||||
|
||||
/* Tag Labels */
|
||||
|
||||
.coral-plugin-tag-label {
|
||||
background-color: #4C1066;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
margin: 10px 10px;
|
||||
margin: 0px 5px;
|
||||
padding: 5px 5px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -213,10 +218,30 @@ hr {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 12px !important;
|
||||
margin-left: 3px;
|
||||
.comment__action-container .material-icons {
|
||||
font-size: 12px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
button.comment__action-button,
|
||||
.comment__action-button button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.comment__action-button[disabled],
|
||||
.comment__action-button[disabled] button {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.comment__action-button--nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.commentStream .material-icons {
|
||||
vertical-align: middle;
|
||||
width: 1em;
|
||||
font-size: 1em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.likedButton {
|
||||
@@ -231,6 +256,7 @@ hr {
|
||||
color: #696969;
|
||||
display: inline-block;
|
||||
font-size: .75rem;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.coral-plugin-permalinks-container {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,139 @@
|
||||
import pym from 'pym.js';
|
||||
|
||||
// This function should return value of window.Coral
|
||||
const Coral = {};
|
||||
const Talk = Coral.Talk = {};
|
||||
|
||||
// build the URL to load in the pym iframe
|
||||
function buildStreamIframeUrl(talkBaseUrl, asset_url, comment, asset_id) {
|
||||
let iframeArray = [
|
||||
talkBaseUrl,
|
||||
(talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/'
|
||||
'embed/stream?asset_url=',
|
||||
encodeURIComponent(asset_url)
|
||||
];
|
||||
|
||||
if (comment) {
|
||||
iframeArray.push('&comment_id=');
|
||||
iframeArray.push(encodeURIComponent(comment));
|
||||
}
|
||||
|
||||
if (asset_id) {
|
||||
iframeArray.push('&asset_id=');
|
||||
iframeArray.push(encodeURIComponent(asset_id));
|
||||
}
|
||||
|
||||
return iframeArray.join('');
|
||||
}
|
||||
|
||||
// Set up postMessage listeners/handlers on the pymParent
|
||||
// e.g. to resize the iframe, and navigate the host page
|
||||
function configurePymParent(pymParent, asset_url) {
|
||||
let notificationOffset = 200;
|
||||
let ready = false;
|
||||
let cachedHeight;
|
||||
|
||||
// Resize parent iframe height when child height changes
|
||||
pymParent.onMessage('height', function(height) {
|
||||
if (height !== cachedHeight) {
|
||||
pymParent.el.firstChild.style.height = `${height}px`;
|
||||
cachedHeight = height;
|
||||
}
|
||||
});
|
||||
|
||||
// Helps child show notifications at the right scrollTop
|
||||
pymParent.onMessage('getPosition', function() {
|
||||
let position = viewport().height + document.body.scrollTop;
|
||||
|
||||
if (position > notificationOffset) {
|
||||
position = position - notificationOffset;
|
||||
}
|
||||
|
||||
pymParent.sendMessage('position', position);
|
||||
});
|
||||
|
||||
// Tell child when parent's DOMContentLoaded
|
||||
pymParent.onMessage('childReady', function () {
|
||||
const interval = setInterval(function () {
|
||||
if (ready) {
|
||||
window.clearInterval(interval);
|
||||
|
||||
// @todo - It's weird to me that this is sent here in addition to the iframe URL. Could it just be in one place?
|
||||
pymParent.sendMessage('DOMContentLoaded', asset_url);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// When end-user clicks link in iframe, open it in parent context
|
||||
pymParent.onMessage('navigate', function (url) {
|
||||
window.open(url, '_blank').focus();
|
||||
});
|
||||
|
||||
// wait till images and other iframes are loaded before scrolling the page.
|
||||
// or do we want to be more aggressive and scroll when we hit DOM ready?
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
ready = true;
|
||||
});
|
||||
|
||||
// get dimensions of viewport
|
||||
const viewport = () => {
|
||||
let e = window, a = 'inner';
|
||||
if ( !( 'innerWidth' in window ) ){
|
||||
a = 'client';
|
||||
e = document.documentElement || document.body;
|
||||
}
|
||||
return {
|
||||
width : e[`${a}Width`],
|
||||
height : e[`${a}Height`]
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Talk stream
|
||||
* @param {HTMLElement} el - Element to render the stream in
|
||||
* @param {Object} opts - Configuration options for talk
|
||||
* @param {String} opts.talk - Talk base URL
|
||||
* @param {String} [opts.title] - Title of Stream (rendered in iframe)
|
||||
* @param {String} [opts.asset_url] - Asset URL
|
||||
* @param {String} [opts.asset_id] - Asset ID
|
||||
*/
|
||||
Talk.render = function (el, opts) {
|
||||
if (!el) {
|
||||
throw new Error('Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.');
|
||||
}
|
||||
if (typeof el !== 'object') {
|
||||
throw new Error(`Coral.Talk.render() expected HTMLElement but got ${el} (${typeof el})`);
|
||||
}
|
||||
opts = opts || {};
|
||||
|
||||
// @todo infer this URL without explicit user input (if possible, may have to be added at build/render time of this script)
|
||||
if (!opts.talk) {
|
||||
throw new Error('Coral.Talk.render() expects opts.talk as the Talk Base URL');
|
||||
}
|
||||
|
||||
// ensure el has an id, as pym can't directly accept the HTMLElement
|
||||
if (!el.id) {
|
||||
el.id = `_${Math.random()}`;
|
||||
}
|
||||
|
||||
let asset_url = opts.asset_url || window.location.href.split('#')[0];
|
||||
let comment = window.location.hash.slice(1);
|
||||
|
||||
let query = {
|
||||
title: opts.title,
|
||||
asset_url: asset_url,
|
||||
id: `${el.id}_iframe`,
|
||||
name: `${el.id}_iframe`
|
||||
};
|
||||
|
||||
if (opts.asset_id && opts.asset_id.length > 0) {
|
||||
query.asset_id = opts.asset_id;
|
||||
}
|
||||
|
||||
let pymParent = new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, asset_url, comment), query);
|
||||
|
||||
configurePymParent(pymParent, asset_url);
|
||||
};
|
||||
|
||||
export default Coral;
|
||||
@@ -46,7 +46,7 @@ const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
|
||||
|
||||
export const fetchSignIn = (formData) => (dispatch) => {
|
||||
dispatch(signInRequest());
|
||||
coralApi('/auth/local', {method: 'POST', body: formData})
|
||||
return coralApi('/auth/local', {method: 'POST', body: formData})
|
||||
.then(({user}) => {
|
||||
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(signInSuccess(user, isAdmin));
|
||||
@@ -148,7 +148,7 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
|
||||
|
||||
export const logout = () => dispatch => {
|
||||
dispatch(logOutRequest());
|
||||
coralApi('/auth', {method: 'DELETE'})
|
||||
return coralApi('/auth', {method: 'DELETE'})
|
||||
.then(() => dispatch(logOutSuccess()))
|
||||
.catch(error => dispatch(logOutFailure(error)));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
mutation AddCommentTag ($id: ID!, $tag: String!) {
|
||||
addCommentTag(id:$id, tag:$tag) {
|
||||
comment {
|
||||
id
|
||||
tags {
|
||||
name
|
||||
}
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import POST_FLAG from './postFlag.graphql';
|
||||
import POST_LIKE from './postLike.graphql';
|
||||
import POST_DONT_AGREE from './postDontAgree.graphql';
|
||||
import DELETE_ACTION from './deleteAction.graphql';
|
||||
import ADD_COMMENT_TAG from './addCommentTag.graphql';
|
||||
import REMOVE_COMMENT_TAG from './removeCommentTag.graphql';
|
||||
|
||||
import commentView from '../fragments/commentView.graphql';
|
||||
|
||||
@@ -40,7 +42,7 @@ export const postComment = graphql(POST_COMMENT, {
|
||||
updateQueries: {
|
||||
AssetQuery: (oldData, {mutationResult:{data:{createComment:{comment}}}}) => {
|
||||
|
||||
if (oldData.asset.moderation === 'PRE') {
|
||||
if (oldData.asset.settings.moderation === 'PRE') {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
@@ -122,3 +124,27 @@ export const deleteAction = graphql(DELETE_ACTION, {
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
export const addCommentTag = graphql(ADD_COMMENT_TAG, {
|
||||
props: ({mutate}) => ({
|
||||
addCommentTag: ({id, tag}) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
id,
|
||||
tag
|
||||
}
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
export const removeCommentTag = graphql(REMOVE_COMMENT_TAG, {
|
||||
props: ({mutate}) => ({
|
||||
removeCommentTag: ({id, tag}) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
id,
|
||||
tag
|
||||
}
|
||||
});
|
||||
}}),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
mutation RemoveCommentTag ($id: ID!, $tag: String!) {
|
||||
removeCommentTag(id:$id, tag:$tag) {
|
||||
comment {
|
||||
id
|
||||
tags {
|
||||
name
|
||||
}
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
query commentQuery($id: ID!) {
|
||||
comment(id: $id) {
|
||||
...commentView
|
||||
parent {
|
||||
...commentView
|
||||
replies {
|
||||
...commentView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@ function getQueryVariable(variable) {
|
||||
}
|
||||
}
|
||||
|
||||
// If no query is included, return a default string for development
|
||||
return 'http://localhost/default/stream';
|
||||
// If not found, return null.
|
||||
return null;
|
||||
}
|
||||
|
||||
export const getCounts = (data) => ({asset_id, limit, sort}) => {
|
||||
@@ -85,11 +85,19 @@ export const loadMore = (data) => ({limit, cursor, parent_id, asset_id, sort}, n
|
||||
};
|
||||
|
||||
export const queryStream = graphql(STREAM_QUERY, {
|
||||
options: () => ({
|
||||
variables: {
|
||||
asset_url: getQueryVariable('asset_url')
|
||||
}
|
||||
}),
|
||||
options: () => {
|
||||
let comment_id = getQueryVariable('comment_id');
|
||||
let has_comment = comment_id != null;
|
||||
|
||||
return {
|
||||
variables: {
|
||||
asset_id: getQueryVariable('asset_id'),
|
||||
asset_url: getQueryVariable('asset_url'),
|
||||
comment_id: has_comment ? comment_id : 'no-comment',
|
||||
has_comment
|
||||
}
|
||||
};
|
||||
},
|
||||
props: ({data}) => ({
|
||||
data,
|
||||
loadMore: loadMore(data),
|
||||
|
||||
@@ -3,6 +3,11 @@ query myCommentHistory {
|
||||
comments {
|
||||
id
|
||||
body
|
||||
asset {
|
||||
id
|
||||
title
|
||||
url
|
||||
}
|
||||
created_at
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
query AssetQuery($asset_url: String!) {
|
||||
asset(url: $asset_url) {
|
||||
query AssetQuery($asset_id: ID, $asset_url: String!, $comment_id: ID!, $has_comment: Boolean!) {
|
||||
comment(id: $comment_id) @include(if: $has_comment) {
|
||||
...commentView
|
||||
parent {
|
||||
...commentView
|
||||
replies {
|
||||
...commentView
|
||||
}
|
||||
}
|
||||
}
|
||||
asset(id: $asset_id, url: $asset_url) {
|
||||
id
|
||||
title
|
||||
url
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
import {Icon} from 'coral-ui';
|
||||
import classnames from 'classnames';
|
||||
|
||||
// tag string for best comments
|
||||
export const BEST_TAG = 'BEST';
|
||||
export const commentIsBest = ({tags} = {}) => {
|
||||
const isBest = Array.isArray(tags) && tags.some(t => t.name === BEST_TAG);
|
||||
return isBest;
|
||||
};
|
||||
|
||||
const name = 'coral-plugin-best';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
// It would be best if the backend/api held this business logic
|
||||
const canModifyBestTag = ({roles = []} = {}) => roles && ['ADMIN', 'MODERATOR'].some(role => roles.includes(role));
|
||||
|
||||
// Put this on a comment to show that it is best
|
||||
|
||||
export const BestIndicator = ({children = <Icon name='star'/>}) => (
|
||||
<span aria-label={lang.t('commentIsBest')}>
|
||||
{ children }
|
||||
</span>
|
||||
);
|
||||
|
||||
/**
|
||||
* Component that only renders children if the provided user prop can modify best tags
|
||||
*/
|
||||
export const IfUserCanModifyBest = ({user, children}) => {
|
||||
if ( ! ( user && canModifyBestTag(user))) {return null;}
|
||||
return children;
|
||||
};
|
||||
|
||||
/**
|
||||
* Button that lets a moderator tag a comment as "Best".
|
||||
* Used to recognize really good comments.
|
||||
*/
|
||||
export class BestButton extends Component {
|
||||
static propTypes = {
|
||||
|
||||
// whether the comment is already tagged as best
|
||||
isBest: PropTypes.bool.isRequired,
|
||||
|
||||
// set that this comment is best
|
||||
addBest: PropTypes.func.isRequired,
|
||||
|
||||
// remove the best status
|
||||
removeBest: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onClickAddBest = this.onClickAddBest.bind(this);
|
||||
this.onClickRemoveBest = this.onClickRemoveBest.bind(this);
|
||||
}
|
||||
|
||||
state = {
|
||||
isSaving: false
|
||||
}
|
||||
|
||||
async onClickAddBest(e) {
|
||||
e.preventDefault();
|
||||
const {addBest} = this.props;
|
||||
if ( ! addBest) {
|
||||
console.warn('BestButton#onClickAddBest called even though there is no addBest prop. doing nothing');
|
||||
return;
|
||||
}
|
||||
this.setState({isSaving: true});
|
||||
try {
|
||||
await addBest();
|
||||
} finally {
|
||||
this.setState({isSaving: false});
|
||||
}
|
||||
}
|
||||
|
||||
async onClickRemoveBest(e) {
|
||||
e.preventDefault();
|
||||
const {removeBest} = this.props;
|
||||
if ( ! removeBest) {
|
||||
console.warn('BestButton#onClickAddBest called even though there is no removeBest prop. doing nothing');
|
||||
return;
|
||||
}
|
||||
this.setState({isSaving: true});
|
||||
try {
|
||||
await removeBest();
|
||||
} finally {
|
||||
this.setState({isSaving: false});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {isBest, addBest, removeBest} = this.props;
|
||||
const {isSaving} = this.state;
|
||||
const disabled = isSaving || ! (isBest ? removeBest : addBest);
|
||||
return (
|
||||
<button onClick={isBest ? this.onClickRemoveBest : this.onClickAddBest}
|
||||
disabled={disabled}
|
||||
className={classnames(`${name}-button`, `e2e__${isBest ? 'unset' : 'set'}-best-comment`)}
|
||||
aria-label={lang.t(isBest ? 'unsetBest' : 'setBest')}>
|
||||
<Icon name={ isBest ? 'star' : 'star_border' } />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"en": {
|
||||
"setBest": "Tag as Best",
|
||||
"unsetBest": "Untag as Best",
|
||||
"commentIsBest": "This comment is one of the best"
|
||||
},
|
||||
"es": {
|
||||
"like": "Establecer como mejor",
|
||||
"liked": "Desarmado como mejor",
|
||||
"commentIsBest": "Este comentario es uno de los mejores"
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ const Comment = props => {
|
||||
return (
|
||||
<div className={styles.myComment}>
|
||||
<p className="myCommentAsset">
|
||||
<a className={`${styles.assetURL} myCommentAnchor`} href='#' onClick={props.link(`${props.asset.url}#${props.comment.id}`)}>{props.asset.url}</a>
|
||||
<a className={`${styles.assetURL} myCommentAnchor`} href='#' onClick={props.link(`${props.asset.url}#${props.comment.id}`)}>{props.asset.title ? props.asset.title : props.asset.url}</a>
|
||||
</p>
|
||||
<p className={`${styles.commentBody} myCommentBody`}>{props.comment.body}</p>
|
||||
</div>
|
||||
@@ -18,7 +18,8 @@ Comment.propTypes = {
|
||||
body: PropTypes.string
|
||||
}).isRequired,
|
||||
asset: PropTypes.shape({
|
||||
url: PropTypes.string
|
||||
url: PropTypes.string,
|
||||
title: PropTypes.string
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const CommentHistory = props => {
|
||||
key={i}
|
||||
comment={comment}
|
||||
link={props.link}
|
||||
asset={props.asset} />;
|
||||
asset={comment.asset} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,8 +19,7 @@ const CommentHistory = props => {
|
||||
};
|
||||
|
||||
CommentHistory.propTypes = {
|
||||
comments: PropTypes.array.isRequired,
|
||||
asset: PropTypes.object.isRequired
|
||||
comments: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default CommentHistory;
|
||||
|
||||
@@ -57,7 +57,7 @@ class LikeButton extends Component {
|
||||
};
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
<button onClick={onLikeClick} className={`${name}-button ${liked && 'likedButton'}`}>
|
||||
<button onClick={onLikeClick} className={`${name}-button ${liked ? 'likedButton' : ''}`}>
|
||||
<span className={`${name}-button-text`}>{lang.t(liked ? 'liked' : 'like')}</span>
|
||||
<i className={`${name}-icon material-icons`}
|
||||
aria-hidden={true}>thumb_up</i>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const name = 'coral-plugin-replies';
|
||||
|
||||
const ReplyButton = ({banned, onClick}) => {
|
||||
return (
|
||||
<button
|
||||
className={`${name}-reply-button`}
|
||||
className={classnames(`${name}-reply-button`)}
|
||||
onClick={onClick}>
|
||||
{lang.t('reply')}
|
||||
<i className={`${name}-icon material-icons`}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const TagLabel = ({isStaff}) => <div className='coral-plugin-tag-label'>
|
||||
{isStaff ? 'Staff' : ''}
|
||||
const TagLabel = ({children}) => <div className='coral-plugin-tag-label'>
|
||||
{children}
|
||||
</div>;
|
||||
|
||||
export default TagLabel;
|
||||
|
||||
@@ -5,9 +5,9 @@ import translations from '../translations';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default ({showSignInDialog}) => (
|
||||
export default ({showSignInDialog, requireEmailConfirmation}) => (
|
||||
<div className={styles.message}>
|
||||
<SignInContainer noButton={true}/>
|
||||
<SignInContainer noButton={true} requireEmailConfirmation={requireEmailConfirmation}/>
|
||||
<div>
|
||||
<a onClick={() => {
|
||||
showSignInDialog();
|
||||
|
||||
@@ -35,7 +35,7 @@ class ProfileContainer extends Component {
|
||||
const {me} = this.props.data;
|
||||
|
||||
if (!loggedIn || !me) {
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog}/>;
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog} requireEmailConfirmation={asset.settings.requireEmailConfirmation}/>;
|
||||
}
|
||||
|
||||
if (data.loading) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import TextField from 'coral-ui/components/TextField';
|
||||
import Alert from './Alert';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import {Dialog, Alert} from 'coral-ui';
|
||||
import FakeComment from './FakeComment';
|
||||
|
||||
import styles from './styles.css';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Alert from './Alert';
|
||||
import {Button, TextField, Spinner, Success} from 'coral-ui';
|
||||
import {Button, TextField, Spinner, Success, Alert} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Alert from './Alert';
|
||||
import {Button, TextField, Spinner, Success} from 'coral-ui';
|
||||
import {Button, TextField, Spinner, Success, Alert} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
@@ -138,12 +137,9 @@ class SignUpContent extends React.Component {
|
||||
</div>
|
||||
}
|
||||
<div className={styles.footer}>
|
||||
<span>
|
||||
{lang.t('signIn.alreadyHaveAnAccount')}
|
||||
<a id="coralSignInViewTrigger" onClick={() => changeView('SIGNIN')}>
|
||||
{lang.t('signIn.signIn')}
|
||||
</a>
|
||||
</span>
|
||||
{lang.t('signIn.alreadyHaveAnAccount')} <a id="coralSignInViewTrigger" onClick={() => changeView('SIGNIN')}>
|
||||
{lang.t('signIn.signIn')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -65,23 +65,6 @@ input.error{
|
||||
padding: 3px 0 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.userBox {
|
||||
padding: 10px 0 20px;
|
||||
letter-spacing: 0.1px;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user