Merge branch 'master' into story-138187767-mod-flag-names

This commit is contained in:
gaba
2017-03-07 13:04:28 +01:00
135 changed files with 3248 additions and 1534 deletions
-1
View File
@@ -5,7 +5,6 @@
],
"plugins": [
"add-module-exports",
"transform-async-to-generator",
"transform-class-properties",
"transform-decorators-legacy",
"transform-object-assign",
+3
View File
@@ -4,6 +4,9 @@
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2017
},
"rules": {
"indent": ["error",
2
+1 -4
View File
@@ -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
+4 -1
View File
@@ -1,6 +1,9 @@
# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](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
+3 -4
View File
@@ -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
View File
@@ -1,6 +1,6 @@
machine:
node:
version: 7
version: 7.6
services:
- docker
- redis
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "../.babelrc",
"plugins": [
"transform-async-to-generator",
]
}
+2 -1
View File
@@ -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"] }]
}
}
+2 -2
View File
@@ -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} />
+26 -2
View File
@@ -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)));
};
+32 -23
View File
@@ -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);
+30 -5
View File
@@ -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>Were sorry, but you dont 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;
+18 -4
View File
@@ -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;
+12 -5
View File
@@ -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 {
+8
View File
@@ -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,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 &rarr;</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;
}
}
@@ -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
}
}
+30 -18
View File
@@ -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
}
}
}
+11 -1
View File
@@ -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;
}
+12 -2
View File
@@ -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);
+13 -2
View File
@@ -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
+22 -10
View File
@@ -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 {
}
+90 -31
View File
@@ -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}
+78 -38
View File
@@ -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);
+2 -1
View File
@@ -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>;
};
+14 -17
View File
@@ -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}
+31 -5
View File
@@ -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
+139
View File
@@ -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;
+2 -2
View File
@@ -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
+106
View File
@@ -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"
}
}
+3 -2
View File
@@ -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;
+1 -1
View File
@@ -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>
+2 -1
View File
@@ -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`}
+2 -2
View File
@@ -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