mirror of
https://github.com/wassname/talk.git
synced 2026-08-14 12:50:17 +08:00
Merge branch 'master' into events
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"exec": "npm-run-all --parallel generate-introspection start:development",
|
||||
"verbose": true,
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
|
||||
"ext": "js,json,graphql,yml",
|
||||
"watch": [
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
FROM coralproject/talk:latest
|
||||
|
||||
# Setup the build arguments
|
||||
ONBUILD ARG TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10
|
||||
ONBUILD ARG TALK_ASSET_COMMENTS_LOAD_DEPTH=10
|
||||
ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
|
||||
ONBUILD ARG TALK_THREADING_LEVEL=3
|
||||
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
|
||||
ONBUILD ARG TALK_DEFAULT_LANG=en
|
||||
|
||||
@@ -4,7 +4,6 @@ const path = require('path');
|
||||
const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
const plugins = require('./services/plugins');
|
||||
const compression = require('compression');
|
||||
const { HELMET_CONFIGURATION } = require('./config');
|
||||
const { MOUNT_PATH } = require('./url');
|
||||
const routes = require('./routes');
|
||||
@@ -64,9 +63,6 @@ app.use(
|
||||
)
|
||||
);
|
||||
|
||||
// Compress the responses if appropriate.
|
||||
app.use(compression());
|
||||
|
||||
//==============================================================================
|
||||
// VIEW CONFIGURATION
|
||||
//==============================================================================
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
//==============================================================================
|
||||
// SIGN IN
|
||||
//==============================================================================
|
||||
|
||||
export const handleLogin = (email, password, recaptchaResponse) => (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, localStorage }
|
||||
) => {
|
||||
dispatch({ type: actions.LOGIN_REQUEST });
|
||||
|
||||
const params = {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
};
|
||||
|
||||
if (recaptchaResponse) {
|
||||
params.headers = {
|
||||
'X-Recaptcha-Response': recaptchaResponse,
|
||||
};
|
||||
}
|
||||
|
||||
return rest('/auth/local', params)
|
||||
.then(({ user, token }) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios && localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
dispatch(handleAuthToken(token));
|
||||
client.resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
|
||||
if (error.translation_key === 'NOT_AUTHORIZED') {
|
||||
// invalid credentials
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: t('error.email_password'),
|
||||
});
|
||||
} else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
dispatch({
|
||||
type: actions.LOGIN_MAXIMUM_EXCEEDED,
|
||||
message: t(`error.${error.translation_key}`),
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// FORGOT PASSWORD
|
||||
//==============================================================================
|
||||
|
||||
const forgotPasswordRequest = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
|
||||
});
|
||||
|
||||
const forgotPasswordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
|
||||
});
|
||||
|
||||
const forgotPasswordFailure = error => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestPasswordReset = email => (dispatch, _, { rest }) => {
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = location.href;
|
||||
|
||||
return rest('/account/password/reset', {
|
||||
method: 'POST',
|
||||
body: { email, loc: redirectUri },
|
||||
})
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(forgotPasswordFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// CHECK LOGIN
|
||||
//==============================================================================
|
||||
|
||||
const checkLoginRequest = () => ({
|
||||
type: actions.CHECK_LOGIN_REQUEST,
|
||||
});
|
||||
|
||||
const checkLoginSuccess = (user, isAdmin) => ({
|
||||
type: actions.CHECK_LOGIN_SUCCESS,
|
||||
user,
|
||||
isAdmin,
|
||||
});
|
||||
|
||||
const checkLoginFailure = error => ({
|
||||
type: actions.CHECK_LOGIN_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const checkLogin = () => (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, localStorage }
|
||||
) => {
|
||||
dispatch(checkLoginRequest());
|
||||
return rest('/auth')
|
||||
.then(({ user }) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios && localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
client.resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// LOGOUT
|
||||
//==============================================================================
|
||||
|
||||
export const logout = () => (dispatch, _, { rest, client, localStorage }) => {
|
||||
return rest('/auth', { method: 'DELETE' }).then(() => {
|
||||
if (localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({ type: actions.LOGOUT });
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// AUTH TOKEN
|
||||
//==============================================================================
|
||||
|
||||
export const handleAuthToken = token => (dispatch, _, { localStorage }) => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
|
||||
|
||||
export const fetchConfig = () => dispatch => {
|
||||
let json = document.getElementById('data');
|
||||
let data = JSON.parse(json.textContent);
|
||||
dispatch({ type: CONFIG_UPDATED, data });
|
||||
};
|
||||
@@ -7,12 +7,12 @@ import t from 'coral-framework/services/i18n';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import cn from 'classnames';
|
||||
|
||||
const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
const CoralDrawer = ({ handleLogout, currentUser }) => (
|
||||
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
|
||||
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
|
||||
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
{can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
{can(currentUser, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
@@ -35,7 +35,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
>
|
||||
{t('configure.community')}
|
||||
</Link>
|
||||
{can(auth.user, 'UPDATE_CONFIG') && (
|
||||
{can(currentUser, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
@@ -55,7 +55,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
CoralDrawer.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool, // hide app elements from a logged out user
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
};
|
||||
|
||||
export default CoralDrawer;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
.header, .cta, .success {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.success {
|
||||
cursor: pointer;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
.signInLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.signInLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ForgotPassword.css';
|
||||
import { Button, TextField, Alert, Success } from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class ForgotPassword extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleEmailChange = e => this.props.onEmailChange(e.target.value);
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleSignInLink = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSignInLink();
|
||||
};
|
||||
|
||||
renderSuccess() {
|
||||
return (
|
||||
<div className={styles.success} onClick={this.handleSignInLink}>
|
||||
{t('password_reset.mail_sent')}{' '}
|
||||
<a
|
||||
className={styles.signInLink}
|
||||
href="#"
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
<Success />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
const { email, errorMessage } = this.props;
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<Button type="submit" cStyle="black" full>
|
||||
Reset Password
|
||||
</Button>
|
||||
<p className={styles.cta}>
|
||||
Go back to{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.signInLink}
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign In
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.success ? this.renderSuccess() : this.renderForm();
|
||||
}
|
||||
}
|
||||
|
||||
ForgotPassword.propTypes = {
|
||||
success: PropTypes.bool.isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
onEmailChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
@@ -13,7 +13,7 @@ import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
const CoralHeader = ({
|
||||
handleLogout,
|
||||
showShortcuts = () => {},
|
||||
auth,
|
||||
currentUser,
|
||||
root,
|
||||
data,
|
||||
}) => {
|
||||
@@ -22,9 +22,9 @@ const CoralHeader = ({
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
|
||||
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
<Navigation className={styles.nav}>
|
||||
{can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
{can(currentUser, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
id="moderateNav"
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
@@ -54,7 +54,7 @@ const CoralHeader = ({
|
||||
<CommunityIndicator root={root} data={data} />
|
||||
</Link>
|
||||
|
||||
{can(auth.user, 'UPDATE_CONFIG') && (
|
||||
{can(currentUser, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id="configureNav"
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
@@ -97,12 +97,14 @@ const CoralHeader = ({
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
className="talk-admin-header-sign-out"
|
||||
>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
{currentUser && (
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
className="talk-admin-header-sign-out"
|
||||
>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
@@ -116,7 +118,7 @@ const CoralHeader = ({
|
||||
};
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
|
||||
@@ -10,22 +10,26 @@ const Layout = ({
|
||||
handleLogout = () => {},
|
||||
toggleShortcutModal = () => {},
|
||||
restricted = false,
|
||||
auth,
|
||||
currentUser,
|
||||
}) => (
|
||||
<LayoutMDL className={styles.layout} fixedDrawer>
|
||||
<Header
|
||||
handleLogout={handleLogout}
|
||||
showShortcuts={toggleShortcutModal}
|
||||
auth={auth}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
<Drawer
|
||||
handleLogout={handleLogout}
|
||||
restricted={restricted}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} auth={auth} />
|
||||
<div className={styles.layout}>{children}</div>
|
||||
</LayoutMDL>
|
||||
);
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
restricted: PropTypes.bool, // hide elements from a user that's logged out
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.layout {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header, .cta {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
import SignIn from '../containers/SignIn';
|
||||
import ForgotPassword from '../containers/ForgotPassword';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Login.css';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
import cn from 'classnames';
|
||||
|
||||
class LoginContainer extends Component {
|
||||
renderForm() {
|
||||
return this.props.forgotPassword ? (
|
||||
<ForgotPassword onSignInLink={this.props.onSignInLink} />
|
||||
) : (
|
||||
<SignIn onForgotPasswordLink={this.props.onForgotPasswordLink} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={cn(styles.layout, 'talk-admin-login')}>
|
||||
<h1 className={styles.header}>Team sign in</h1>
|
||||
<p className={styles.cta}>Sign in to interact with your community.</p>
|
||||
{this.renderForm()}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginContainer.propTypes = {
|
||||
forgotPassword: PropTypes.bool.isRequired,
|
||||
onForgotPasswordLink: PropTypes.func.isRequired,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default LoginContainer;
|
||||
@@ -3,35 +3,3 @@
|
||||
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;
|
||||
}
|
||||
|
||||
.loginHeader {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
cursor: pointer;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.forgotPasswordCTA {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.forgotPasswordLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.forgotPasswordLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.recaptcha {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './SignIn.css';
|
||||
import { Button, TextField, Alert } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import Recaptcha from 'coral-framework/components/Recaptcha';
|
||||
|
||||
class SignIn extends React.Component {
|
||||
recaptcha = null;
|
||||
|
||||
handleForgotPasswordLink = e => {
|
||||
e.preventDefault();
|
||||
this.props.onForgotPasswordLink();
|
||||
};
|
||||
handleEmailChange = e => this.props.onEmailChange(e.target.value);
|
||||
handlePasswordChange = e => this.props.onPasswordChange(e.target.value);
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
|
||||
// Reset recaptcha because each response can only
|
||||
// be used once.
|
||||
if (this.recaptcha) {
|
||||
this.recaptcha.reset();
|
||||
}
|
||||
};
|
||||
|
||||
handleRecaptchaRef = ref => {
|
||||
this.recaptcha = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { email, password, errorMessage, requireRecaptcha } = this.props;
|
||||
return (
|
||||
<form className="talk-admin-login-sign-in" onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={this.handlePasswordChange}
|
||||
type="password"
|
||||
/>
|
||||
{requireRecaptcha && (
|
||||
<div className={styles.recaptcha}>
|
||||
<Recaptcha
|
||||
ref={this.handleRecaptchaRef}
|
||||
onVerify={this.props.onRecaptchaVerify}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className={cn(styles.signInButton, 'talk-admin-login-sign-in-button')}
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
full
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password?{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
>
|
||||
Request a new one.
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SignIn.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
password: PropTypes.string.isRequired,
|
||||
onEmailChange: PropTypes.func.isRequired,
|
||||
onPasswordChange: PropTypes.func.isRequired,
|
||||
onForgotPasswordLink: PropTypes.func.isRequired,
|
||||
onRecaptchaVerify: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
requireRecaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withForgotPassword } from 'coral-framework/hocs';
|
||||
import { compose } from 'recompose';
|
||||
import ForgotPassword from '../components/ForgotPassword';
|
||||
|
||||
class ForgotPasswordContainer extends Component {
|
||||
state = {
|
||||
email: '',
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.forgotPassword(this.state.email);
|
||||
};
|
||||
|
||||
handleEmailChange = email => {
|
||||
this.setState({ email });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ForgotPassword
|
||||
onSubmit={this.handleSubmit}
|
||||
onEmailChange={this.handleEmailChange}
|
||||
email={this.state.email}
|
||||
errorMessage={this.props.errorMessage}
|
||||
success={this.props.success}
|
||||
onSignInLink={this.props.onSignInLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ForgotPasswordContainer.propTypes = {
|
||||
success: PropTypes.bool.isRequired,
|
||||
forgotPassword: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default compose(withForgotPassword)(ForgotPasswordContainer);
|
||||
@@ -2,86 +2,58 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import Layout from '../components/Layout';
|
||||
import { fetchConfig } from '../actions/config';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
import Login from '../containers/Login';
|
||||
import { FullLoading } from '../components/FullLoading';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
|
||||
import {
|
||||
checkLogin,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
logout,
|
||||
} from '../actions/auth';
|
||||
import { logout } from 'coral-framework/actions/auth';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import UserDetail from 'coral-admin/src/containers/UserDetail';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class LayoutContainer extends React.Component {
|
||||
componentWillMount() {
|
||||
const { checkLogin, fetchConfig } = this.props;
|
||||
|
||||
checkLogin();
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
loggedIn,
|
||||
loadingUser,
|
||||
loginError,
|
||||
loginMaxExceeded,
|
||||
passwordRequestSuccess,
|
||||
} = this.props.auth;
|
||||
|
||||
const {
|
||||
currentUser,
|
||||
checkedInitialLogin,
|
||||
children,
|
||||
logout,
|
||||
toggleShortcutModal,
|
||||
TALK_RECAPTCHA_PUBLIC,
|
||||
} = this.props;
|
||||
|
||||
if (loadingUser) {
|
||||
if (!checkedInitialLogin) {
|
||||
return <FullLoading />;
|
||||
}
|
||||
|
||||
if (!loggedIn) {
|
||||
return (
|
||||
<AdminLogin
|
||||
loginMaxExceeded={loginMaxExceeded}
|
||||
handleLogin={this.props.handleLogin}
|
||||
requestPasswordReset={this.props.requestPasswordReset}
|
||||
passwordRequestSuccess={passwordRequestSuccess}
|
||||
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
|
||||
errorMessage={loginError}
|
||||
/>
|
||||
);
|
||||
if (!currentUser) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
if (can(user, 'ACCESS_ADMIN') && loggedIn) {
|
||||
return (
|
||||
<Layout
|
||||
handleLogout={logout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
auth={this.props.auth}
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
return (
|
||||
<Layout handleLogout={logout} {...this.props}>
|
||||
<p>
|
||||
This page is for team use only. Please contact an administrator if
|
||||
you want to join this team.
|
||||
</p>
|
||||
</Layout>
|
||||
);
|
||||
if (currentUser) {
|
||||
if (can(currentUser, 'ACCESS_ADMIN')) {
|
||||
return (
|
||||
<Layout
|
||||
handleLogout={logout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
currentUser={this.props.currentUser}
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Layout {...this.props} handleLogout={logout}>
|
||||
<p>
|
||||
This page is for team use only. Please contact an administrator if
|
||||
you want to join this team.
|
||||
</p>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <FullLoading />;
|
||||
}
|
||||
@@ -89,29 +61,20 @@ class LayoutContainer extends React.Component {
|
||||
|
||||
LayoutContainer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
requestPasswordReset: PropTypes.func,
|
||||
handleLogin: PropTypes.func,
|
||||
auth: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
currentUser: PropTypes.object,
|
||||
checkedInitialLogin: PropTypes.bool,
|
||||
logout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
|
||||
checkLogin: PropTypes.func,
|
||||
fetchConfig: PropTypes.func,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth,
|
||||
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
|
||||
currentUser: state.auth.user,
|
||||
checkedInitialLogin: state.auth.checkedInitialLogin,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
checkLogin,
|
||||
fetchConfig,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
toggleShortcutModal,
|
||||
logout,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
import Login from '../components/Login';
|
||||
|
||||
class LoginContainer extends Component {
|
||||
state = {
|
||||
forgotPassword: false,
|
||||
};
|
||||
|
||||
switchToForgotPassword = () => {
|
||||
this.setState({ forgotPassword: true });
|
||||
};
|
||||
|
||||
switchToSignIn = () => {
|
||||
this.setState({ forgotPassword: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Login
|
||||
forgotPassword={this.state.forgotPassword}
|
||||
onForgotPasswordLink={this.switchToForgotPassword}
|
||||
onSignInLink={this.switchToSignIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginContainer.propTypes = {};
|
||||
|
||||
export default LoginContainer;
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withSignIn } from 'coral-framework/hocs';
|
||||
import { compose } from 'recompose';
|
||||
import SignIn from '../components/SignIn';
|
||||
|
||||
class SignInContainer extends Component {
|
||||
state = {
|
||||
email: '',
|
||||
password: '',
|
||||
recaptchaResponse: null,
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.signIn(
|
||||
this.state.email,
|
||||
this.state.password,
|
||||
this.state.recaptchaResponse
|
||||
);
|
||||
};
|
||||
|
||||
handleEmailChange = email => {
|
||||
this.setState({ email });
|
||||
};
|
||||
|
||||
handlePasswordChange = password => {
|
||||
this.setState({ password });
|
||||
};
|
||||
|
||||
handleRecaptchaVerify = recaptchaResponse => {
|
||||
this.setState({ recaptchaResponse });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SignIn
|
||||
onSubmit={this.handleSubmit}
|
||||
onEmailChange={this.handleEmailChange}
|
||||
onPasswordChange={this.handlePasswordChange}
|
||||
email={this.state.email}
|
||||
password={this.state.password}
|
||||
errorMessage={this.props.errorMessage}
|
||||
onForgotPasswordLink={this.props.onForgotPasswordLink}
|
||||
onRecaptchaVerify={this.handleRecaptchaVerify}
|
||||
requireRecaptcha={this.props.requireRecaptcha}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SignInContainer.propTypes = {
|
||||
signIn: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onForgotPasswordLink: PropTypes.func.isRequired,
|
||||
requireRecaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default compose(withSignIn)(SignInContainer);
|
||||
@@ -1,65 +0,0 @@
|
||||
import * as actions from '../constants/auth';
|
||||
|
||||
const initialState = {
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
loginError: null,
|
||||
loginMaxExceeded: false,
|
||||
passwordRequestSuccess: null,
|
||||
};
|
||||
|
||||
export default function auth(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CHECK_LOGIN_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
loadingUser: true,
|
||||
};
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loggedIn: false,
|
||||
loadingUser: false,
|
||||
user: null,
|
||||
};
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loggedIn: true,
|
||||
loadingUser: false,
|
||||
user: action.user,
|
||||
};
|
||||
case actions.LOGOUT:
|
||||
return initialState;
|
||||
case actions.LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loginMaxExceeded: false,
|
||||
loginError: null,
|
||||
};
|
||||
case actions.LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loginError: action.message,
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
passwordRequestSuccess: null,
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
passwordRequestSuccess:
|
||||
'If you have a registered account, a password reset link was sent to that email.',
|
||||
};
|
||||
case actions.LOGIN_MAXIMUM_EXCEEDED:
|
||||
return {
|
||||
...state,
|
||||
loginMaxExceeded: true,
|
||||
loginError: action.message,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
import auth from './auth';
|
||||
import stories from './stories';
|
||||
import configure from './configure';
|
||||
import community from './community';
|
||||
import moderation from './moderation';
|
||||
import install from './install';
|
||||
import config from './config';
|
||||
import banUserDialog from './banUserDialog';
|
||||
import suspendUserDialog from './suspendUserDialog';
|
||||
import userDetail from './userDetail';
|
||||
import ui from './ui';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
banUserDialog,
|
||||
configure,
|
||||
suspendUserDialog,
|
||||
@@ -20,6 +17,5 @@ export default {
|
||||
community,
|
||||
moderation,
|
||||
install,
|
||||
config,
|
||||
ui,
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class Configure extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
auth: { user },
|
||||
currentUser,
|
||||
canSave,
|
||||
savePending,
|
||||
setActiveSection,
|
||||
@@ -32,7 +32,7 @@ export default class Configure extends Component {
|
||||
} = this.props;
|
||||
const SectionComponent = this.getSectionComponent(activeSection);
|
||||
|
||||
if (!can(user, 'UPDATE_CONFIG')) {
|
||||
if (!can(currentUser, 'UPDATE_CONFIG')) {
|
||||
return (
|
||||
<p>
|
||||
You must be an administrator to access config settings. Please find
|
||||
@@ -87,7 +87,7 @@ export default class Configure extends Component {
|
||||
|
||||
Configure.propTypes = {
|
||||
savePending: PropTypes.func.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
|
||||
@@ -30,7 +30,7 @@ class ConfigureContainer extends Component {
|
||||
|
||||
return (
|
||||
<Configure
|
||||
auth={this.props.auth}
|
||||
currentUser={this.props.currentUser}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
settings={this.props.mergedSettings}
|
||||
@@ -71,7 +71,7 @@ const withConfigureQuery = withQuery(
|
||||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth,
|
||||
currentUser: state.auth.user,
|
||||
pending: state.configure.pending,
|
||||
canSave: state.configure.canSave,
|
||||
activeSection: state.configure.activeSection,
|
||||
@@ -97,7 +97,7 @@ ConfigureContainer.propTypes = {
|
||||
updateSettings: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
setActiveSection: PropTypes.func.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -143,7 +143,6 @@
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
top: 2px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ class Moderation extends Component {
|
||||
commentBelongToQueue={this.props.commentBelongToQueue}
|
||||
isLoadingMore={this.state.isLoadingMore}
|
||||
commentCount={activeTabCount}
|
||||
currentUserId={this.props.auth.user.id}
|
||||
currentUserId={this.props.currentUser.id}
|
||||
viewUserDetail={viewUserDetail}
|
||||
selectCommentId={props.selectCommentId}
|
||||
cleanUpQueue={props.cleanUpQueue}
|
||||
@@ -225,7 +225,7 @@ Moderation.propTypes = {
|
||||
cleanUpQueue: PropTypes.func.isRequired,
|
||||
storySearchChange: PropTypes.func.isRequired,
|
||||
moderation: PropTypes.object.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
queueConfig: PropTypes.object.isRequired,
|
||||
commentBelongToQueue: PropTypes.func.isRequired,
|
||||
handleCommentChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -110,7 +110,7 @@ class ModerationContainer extends Component {
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
this.props.currentUser.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_accepted',
|
||||
@@ -131,7 +131,7 @@ class ModerationContainer extends Component {
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
this.props.currentUser.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_rejected',
|
||||
@@ -152,7 +152,7 @@ class ModerationContainer extends Component {
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
this.props.currentUser.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_reset',
|
||||
@@ -515,7 +515,7 @@ const withModQueueQuery = withQuery(
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
moderation: state.moderation,
|
||||
auth: state.auth,
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Router, Route } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Embed from './containers/Embed';
|
||||
import { LoginContainer } from './containers/LoginContainer';
|
||||
|
||||
const routes = (
|
||||
<div>
|
||||
<Route exact path="/embed/stream/login" component={LoginContainer} />
|
||||
<Route path="*" component={Embed} />
|
||||
</div>
|
||||
);
|
||||
|
||||
class AppRouter extends React.Component {
|
||||
static contextTypes = {
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Router history={this.context.history} routes={routes} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default AppRouter;
|
||||
@@ -1,415 +0,0 @@
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import get from 'lodash/get';
|
||||
|
||||
export const updateStatus = status => ({
|
||||
type: actions.UPDATE_STATUS,
|
||||
status,
|
||||
});
|
||||
|
||||
export const showSignInDialog = () => ({
|
||||
type: actions.SHOW_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const hideSignInDialog = () => dispatch => {
|
||||
if (window.opener && window.opener !== window) {
|
||||
// TODO: We need to address this when we refactor the
|
||||
// login popup out of the embed.
|
||||
|
||||
// we are in a popup
|
||||
window.close();
|
||||
} else {
|
||||
dispatch(checkLogin());
|
||||
}
|
||||
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
|
||||
};
|
||||
|
||||
export const resetSignInDialog = () => dispatch => {
|
||||
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
|
||||
};
|
||||
|
||||
export const focusSignInDialog = () => ({
|
||||
type: actions.FOCUS_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const blurSignInDialog = () => ({
|
||||
type: actions.BLUR_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const showCreateUsernameDialog = () => ({
|
||||
type: actions.SHOW_CREATEUSERNAME_DIALOG,
|
||||
});
|
||||
|
||||
export const hideCreateUsernameDialog = () => ({
|
||||
type: actions.HIDE_CREATEUSERNAME_DIALOG,
|
||||
});
|
||||
|
||||
export const updateUsername = username => ({
|
||||
type: actions.UPDATE_USERNAME,
|
||||
username,
|
||||
});
|
||||
|
||||
export const changeView = view => dispatch => {
|
||||
dispatch({
|
||||
type: actions.CHANGE_VIEW,
|
||||
view,
|
||||
});
|
||||
|
||||
switch (view) {
|
||||
case 'SIGNUP':
|
||||
window.resizeTo(500, 800);
|
||||
break;
|
||||
case 'FORGOT':
|
||||
window.resizeTo(500, 400);
|
||||
break;
|
||||
default:
|
||||
window.resizeTo(500, 550);
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanState = () => ({
|
||||
type: actions.CLEAN_STATE,
|
||||
});
|
||||
|
||||
// Sign In Actions
|
||||
|
||||
const signInRequest = email => ({
|
||||
type: actions.FETCH_SIGNIN_REQUEST,
|
||||
email,
|
||||
});
|
||||
|
||||
const signInFailure = error => ({
|
||||
type: actions.FETCH_SIGNIN_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// AUTH TOKEN
|
||||
//==============================================================================
|
||||
|
||||
export const handleAuthToken = token => (dispatch, _, { localStorage }) => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// SIGN IN
|
||||
//==============================================================================
|
||||
|
||||
export const fetchSignIn = formData => {
|
||||
return (dispatch, _, { rest }) => {
|
||||
dispatch(signInRequest(formData.email));
|
||||
|
||||
return rest('/auth/local', { method: 'POST', body: formData })
|
||||
.then(({ token }) => {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
dispatch(handleAuthToken(token));
|
||||
}
|
||||
dispatch(hideSignInDialog());
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
if (error.metadata) {
|
||||
// the user might not have a valid email. prompt the user user re-request the confirmation email
|
||||
dispatch(
|
||||
signInFailure(t('error.email_not_verified', error.metadata))
|
||||
);
|
||||
} else if (error.translation_key === 'NOT_AUTHORIZED') {
|
||||
// invalid credentials
|
||||
dispatch(signInFailure(t('error.email_password'), error.metadata));
|
||||
} else {
|
||||
dispatch(signInFailure(error));
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// SIGN IN - FACEBOOK
|
||||
//==============================================================================
|
||||
|
||||
const signInFacebookRequest = () => ({
|
||||
type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST,
|
||||
});
|
||||
|
||||
const signInFacebookSuccess = user => ({
|
||||
type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS,
|
||||
user,
|
||||
});
|
||||
|
||||
const signInFacebookFailure = error => ({
|
||||
type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchSignInFacebook = () => (dispatch, _, { rest }) => {
|
||||
dispatch(signInFacebookRequest());
|
||||
window.open(
|
||||
`${rest.uri}/auth/facebook`,
|
||||
'Continue with Facebook',
|
||||
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
|
||||
);
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// SIGN UP - FACEBOOK
|
||||
//==============================================================================
|
||||
|
||||
const signUpFacebookRequest = () => ({
|
||||
type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchSignUpFacebook = () => (dispatch, _, { rest }) => {
|
||||
dispatch(signUpFacebookRequest());
|
||||
window.open(
|
||||
`${rest.uri}/auth/facebook`,
|
||||
'Continue with Facebook',
|
||||
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
|
||||
);
|
||||
};
|
||||
|
||||
export const facebookCallback = (err, data) => dispatch => {
|
||||
if (err) {
|
||||
dispatch(signInFacebookFailure(err));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
dispatch(handleAuthToken(data.token));
|
||||
dispatch(signInFacebookSuccess(data.user));
|
||||
dispatch(hideSignInDialog());
|
||||
} catch (err) {
|
||||
dispatch(signInFacebookFailure(err));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// SIGN UP
|
||||
//==============================================================================
|
||||
|
||||
const signUpRequest = () => ({ type: actions.FETCH_SIGNUP_REQUEST });
|
||||
const signUpSuccess = user => ({ type: actions.FETCH_SIGNUP_SUCCESS, user });
|
||||
const signUpFailure = error => ({ type: actions.FETCH_SIGNUP_FAILURE, error });
|
||||
|
||||
export const fetchSignUp = formData => (dispatch, getState, { rest }) => {
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
dispatch(signUpRequest());
|
||||
|
||||
rest('/users', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Pym-Url': redirectUri },
|
||||
})
|
||||
.then(({ user }) => {
|
||||
dispatch(signUpSuccess(user));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(signUpFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// FORGOT PASSWORD
|
||||
//==============================================================================
|
||||
|
||||
const forgotPasswordRequest = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
|
||||
});
|
||||
|
||||
const forgotPasswordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
|
||||
});
|
||||
|
||||
const forgotPasswordFailure = error => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchForgotPassword = email => (dispatch, getState, { rest }) => {
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
rest('/account/password/reset', {
|
||||
method: 'POST',
|
||||
body: { email, loc: redirectUri },
|
||||
})
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(forgotPasswordFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// LOGOUT
|
||||
//==============================================================================
|
||||
|
||||
export const logout = () => async (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, pym, localStorage }
|
||||
) => {
|
||||
await rest('/auth', { method: 'DELETE' });
|
||||
|
||||
if (localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({ type: actions.LOGOUT });
|
||||
pym.sendMessage('coral-auth-changed');
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// CHECK LOGIN
|
||||
//==============================================================================
|
||||
|
||||
const checkLoginRequest = () => ({ type: actions.CHECK_LOGIN_REQUEST });
|
||||
const checkLoginFailure = error => ({
|
||||
type: actions.CHECK_LOGIN_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
const checkLoginSuccess = (user, isAdmin) => ({
|
||||
type: actions.CHECK_LOGIN_SUCCESS,
|
||||
user,
|
||||
isAdmin,
|
||||
});
|
||||
|
||||
const ErrNotLoggedIn = new Error('Not logged in');
|
||||
|
||||
export const checkLogin = () => (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, pym, localStorage }
|
||||
) => {
|
||||
dispatch(checkLoginRequest());
|
||||
rest('/auth')
|
||||
.then(result => {
|
||||
if (!result.user) {
|
||||
if (localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
throw ErrNotLoggedIn;
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch(checkLoginSuccess(result.user));
|
||||
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
|
||||
|
||||
// This is for login via social. Usernames should be set.
|
||||
if (
|
||||
get(result.user, 'status.username.status') === 'UNSET' &&
|
||||
!get(result.user, 'status.banned.status')
|
||||
) {
|
||||
dispatch(showCreateUsernameDialog());
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error !== ErrNotLoggedIn) {
|
||||
console.error(error);
|
||||
}
|
||||
if (error.status && error.status === 401 && localStorage) {
|
||||
// Unauthorized.
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
export const validForm = () => ({ type: actions.VALID_FORM });
|
||||
export const invalidForm = error => ({ type: actions.INVALID_FORM, error });
|
||||
|
||||
//==============================================================================
|
||||
// VERIFY EMAIL
|
||||
//==============================================================================
|
||||
|
||||
const verifyEmailRequest = () => ({
|
||||
type: actions.VERIFY_EMAIL_REQUEST,
|
||||
});
|
||||
|
||||
const verifyEmailSuccess = () => ({
|
||||
type: actions.VERIFY_EMAIL_SUCCESS,
|
||||
});
|
||||
|
||||
const verifyEmailFailure = error => ({
|
||||
type: actions.VERIFY_EMAIL_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestConfirmEmail = email => (dispatch, getState, { rest }) => {
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
dispatch(verifyEmailRequest());
|
||||
return rest('/users/resend-verify', {
|
||||
method: 'POST',
|
||||
body: { email },
|
||||
headers: { 'X-Pym-Url': redirectUri },
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(verifyEmailSuccess());
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(verifyEmailFailure(error));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// Login Popup actions.
|
||||
export const setRequireEmailVerification = required => ({
|
||||
type: actions.SET_REQUIRE_EMAIL_VERIFICATION,
|
||||
required,
|
||||
});
|
||||
|
||||
export const setRedirectUri = uri => ({
|
||||
type: actions.SET_REDIRECT_URI,
|
||||
uri,
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// Edit Username
|
||||
//==============================================================================
|
||||
|
||||
const editUsernameFailure = error => ({
|
||||
type: actions.EDIT_USERNAME_FAILURE,
|
||||
error,
|
||||
});
|
||||
const editUsernameSuccess = () => ({ type: actions.EDIT_USERNAME_SUCCESS });
|
||||
|
||||
export const editName = username => (dispatch, _, { rest }) => {
|
||||
return rest('/account/username', { method: 'PUT', body: { username } })
|
||||
.then(() => {
|
||||
dispatch(editUsernameSuccess());
|
||||
dispatch(notify('success', t('framework.success_name_update')));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(editUsernameFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as actions from '../constants/login';
|
||||
import { checkLogin } from 'coral-framework/actions/auth';
|
||||
|
||||
export const showSignInDialog = () => ({
|
||||
type: actions.SHOW_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const hideSignInDialog = () => dispatch => {
|
||||
dispatch(checkLogin());
|
||||
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
|
||||
};
|
||||
|
||||
export const focusSignInDialog = () => ({
|
||||
type: actions.FOCUS_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const blurSignInDialog = () => ({
|
||||
type: actions.BLUR_SIGNIN_DIALOG,
|
||||
});
|
||||
@@ -16,17 +16,10 @@ import cn from 'classnames';
|
||||
|
||||
export default class Embed extends React.Component {
|
||||
changeTab = tab => {
|
||||
// TODO: move data fetching to appropiate containers.
|
||||
switch (tab) {
|
||||
case 'profile':
|
||||
this.props.data.refetch();
|
||||
break;
|
||||
}
|
||||
this.props.setActiveTab(tab);
|
||||
};
|
||||
|
||||
getTabs() {
|
||||
const { user } = this.props.auth;
|
||||
const tabs = [
|
||||
<Tab
|
||||
key="stream"
|
||||
@@ -43,7 +36,7 @@ export default class Embed extends React.Component {
|
||||
{t('framework.my_profile')}
|
||||
</Tab>,
|
||||
];
|
||||
if (can(user, 'UPDATE_ASSET_CONFIG')) {
|
||||
if (can(this.props.currentUser, 'UPDATE_ASSET_CONFIG')) {
|
||||
tabs.push(
|
||||
<Tab
|
||||
key="config"
|
||||
@@ -64,11 +57,12 @@ export default class Embed extends React.Component {
|
||||
root,
|
||||
root: { asset },
|
||||
data,
|
||||
auth: { showSignInDialog, signInDialogFocus },
|
||||
showSignInDialog,
|
||||
signInDialogFocus,
|
||||
blurSignInDialog,
|
||||
focusSignInDialog,
|
||||
hideSignInDialog,
|
||||
router: { location: { query: { parentUrl } } },
|
||||
parentUrl,
|
||||
} = this.props;
|
||||
const hasHighlightedComment = !!commentId;
|
||||
|
||||
@@ -81,9 +75,7 @@ export default class Embed extends React.Component {
|
||||
<AutomaticAssetClosure asset={asset} />
|
||||
<IfSlotIsNotEmpty slot="login">
|
||||
<Popup
|
||||
href={`embed/stream/login?parentUrl=${encodeURIComponent(
|
||||
parentUrl
|
||||
)}`}
|
||||
href={`login?parentUrl=${encodeURIComponent(parentUrl)}`}
|
||||
title="Login"
|
||||
features="menubar=0,resizable=0,width=500,height=550,top=200,left=500"
|
||||
open={showSignInDialog}
|
||||
@@ -138,11 +130,13 @@ export default class Embed extends React.Component {
|
||||
|
||||
Embed.propTypes = {
|
||||
setActiveTab: PropTypes.func,
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
showSignInDialog: PropTypes.bool,
|
||||
signInDialogFocus: PropTypes.bool,
|
||||
blurSignInDialog: PropTypes.func,
|
||||
focusSignInDialog: PropTypes.func,
|
||||
hideSignInDialog: PropTypes.func,
|
||||
router: PropTypes.object,
|
||||
parentUrl: PropTypes.string,
|
||||
commentId: PropTypes.string,
|
||||
root: PropTypes.object,
|
||||
activeTab: PropTypes.string,
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
export const CHANGE_VIEW = 'CHANGE_VIEW';
|
||||
export const CLEAN_STATE = 'CLEAN_STATE';
|
||||
|
||||
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
|
||||
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
|
||||
export const FOCUS_SIGNIN_DIALOG = 'FOCUS_SIGNIN_DIALOG';
|
||||
export const BLUR_SIGNIN_DIALOG = 'BLUR_SIGNIN_DIALOG';
|
||||
|
||||
export const CREATE_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST';
|
||||
export const CREATE_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS';
|
||||
export const CREATE_USERNAME_FAILURE = 'CREATE_USERNAME_FAILURE';
|
||||
export const CREATE_USERNAME = 'CREATE_USERNAME';
|
||||
export const SHOW_CREATEUSERNAME_DIALOG = 'SHOW_CREATEUSERNAME_DIALOG';
|
||||
export const HIDE_CREATEUSERNAME_DIALOG = 'HIDE_CREATEUSERNAME_DIALOG';
|
||||
|
||||
export const EDIT_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST';
|
||||
export const EDIT_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS';
|
||||
export const EDIT_USERNAME_FAILURE = 'CREATE_USERNAME_FAILURE';
|
||||
export const EDIT_USERNAME = 'CREATE_USERNAME';
|
||||
|
||||
export const FETCH_SIGNUP_REQUEST = 'FETCH_SIGNUP_REQUEST';
|
||||
export const FETCH_SIGNUP_FAILURE = 'FETCH_SIGNUP_FAILURE';
|
||||
export const FETCH_SIGNUP_SUCCESS = 'FETCH_SIGNUP_SUCCESS';
|
||||
|
||||
export const FETCH_SIGNIN_REQUEST = 'FETCH_SIGNIN_REQUEST';
|
||||
export const FETCH_SIGNIN_FAILURE = 'FETCH_SIGNIN_FAILURE';
|
||||
export const FETCH_SIGNIN_SUCCESS = 'FETCH_SIGNIN_SUCCESS';
|
||||
|
||||
export const FETCH_SIGNIN_FACEBOOK_REQUEST = 'FETCH_SIGNIN_FACEBOOK_REQUEST';
|
||||
export const FETCH_SIGNIN_FACEBOOK_FAILURE = 'FETCH_SIGNIN_FACEBOOK_FAILURE';
|
||||
export const FETCH_SIGNIN_FACEBOOK_SUCCESS = 'FETCH_SIGNIN_FACEBOOK_SUCCESS';
|
||||
|
||||
export const FETCH_SIGNUP_FACEBOOK_REQUEST = 'FETCH_SIGNUP_FACEBOOK_REQUEST';
|
||||
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';
|
||||
|
||||
export const LOGOUT = 'LOGOUT';
|
||||
|
||||
export const INVALID_FORM = 'INVALID_FORM';
|
||||
export const VALID_FORM = 'VALID_FORM';
|
||||
|
||||
export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
|
||||
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
|
||||
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
|
||||
|
||||
export const VERIFY_EMAIL_REQUEST = 'VERIFY_EMAIL_REQUEST';
|
||||
export const VERIFY_EMAIL_SUCCESS = 'VERIFY_EMAIL_SUCCESS';
|
||||
export const VERIFY_EMAIL_FAILURE = 'VERIFY_EMAIL_FAILURE';
|
||||
export const UPDATE_USERNAME = 'UPDATE_USERNAME';
|
||||
|
||||
// Login Popup actions.
|
||||
export const SET_REQUIRE_EMAIL_VERIFICATION = 'SET_REQUIRE_EMAIL_VERIFICATION';
|
||||
export const SET_REDIRECT_URI = 'SET_REDIRECT_URI';
|
||||
|
||||
export const RESET_SIGNIN_DIALOG = 'RESET_SIGNIN_DIALOG';
|
||||
export const UPDATE_STATUS = 'UPDATE_STATUS';
|
||||
@@ -0,0 +1,4 @@
|
||||
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
|
||||
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
|
||||
export const FOCUS_SIGNIN_DIALOG = 'FOCUS_SIGNIN_DIALOG';
|
||||
export const BLUR_SIGNIN_DIALOG = 'BLUR_SIGNIN_DIALOG';
|
||||
@@ -1,14 +1,27 @@
|
||||
import defaultTo from 'lodash/defaultTo';
|
||||
|
||||
const prefix = 'TALK_EMBED_STREAM';
|
||||
|
||||
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
|
||||
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
|
||||
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
|
||||
export const VIEW_COMMENT = 'VIEW_COMMENT';
|
||||
export const ADDTL_COMMENTS_ON_LOAD_MORE = parseInt(
|
||||
defaultTo(process.env.TALK_ADDTL_COMMENTS_ON_LOAD_MORE, '10')
|
||||
);
|
||||
export const ASSET_COMMENTS_LOAD_DEPTH = parseInt(
|
||||
defaultTo(process.env.TALK_ASSET_COMMENTS_LOAD_DEPTH, '10')
|
||||
);
|
||||
export const REPLY_COMMENTS_LOAD_DEPTH = parseInt(
|
||||
defaultTo(process.env.TALK_REPLY_COMMENTS_LOAD_DEPTH, '3')
|
||||
);
|
||||
export const THREADING_LEVEL = parseInt(
|
||||
defaultTo(process.env.TALK_THREADING_LEVEL, '3')
|
||||
);
|
||||
|
||||
export const ADD_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_ADD_TAG`;
|
||||
export const ADD_COMMENT_CLASSNAME = 'ADD_COMMENT_CLASSNAME';
|
||||
export const CLEAR_COMMENT_BOX_TAGS = `${prefix}_COMMENT_BOX_CLEAR_TAGS`;
|
||||
export const REMOVE_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_REMOVE_TAG`;
|
||||
export const REMOVE_COMMENT_CLASSNAME = 'REMOVE_COMMENT_CLASSNAME';
|
||||
export const THREADING_LEVEL = process.env.TALK_THREADING_LEVEL;
|
||||
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
|
||||
export const SET_ACTIVE_TAB = 'CORAL_STREAM_SET_ACTIVE_TAB';
|
||||
export const SET_SORT = 'CORAL_STREAM_SET_SORT';
|
||||
export const ADD_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_ADD_TAG`;
|
||||
export const REMOVE_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_REMOVE_TAG`;
|
||||
export const CLEAR_COMMENT_BOX_TAGS = `${prefix}_COMMENT_BOX_CLEAR_TAGS`;
|
||||
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
|
||||
export const VIEW_COMMENT = 'VIEW_COMMENT';
|
||||
|
||||
@@ -8,8 +8,13 @@ import branch from 'recompose/branch';
|
||||
import renderComponent from 'recompose/renderComponent';
|
||||
|
||||
import { Spinner } from 'coral-ui';
|
||||
import * as authActions from '../actions/auth';
|
||||
import * as assetActions from '../actions/asset';
|
||||
import {
|
||||
focusSignInDialog,
|
||||
blurSignInDialog,
|
||||
hideSignInDialog,
|
||||
} from '../actions/login';
|
||||
import { updateStatus } from 'coral-framework/actions/auth';
|
||||
import { fetchAssetSuccess } from '../actions/asset';
|
||||
import {
|
||||
getDefinitionName,
|
||||
getSlotFragmentSpreads,
|
||||
@@ -24,16 +29,6 @@ import t from 'coral-framework/services/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { setActiveTab } from '../actions/embed';
|
||||
|
||||
const {
|
||||
logout,
|
||||
checkLogin,
|
||||
focusSignInDialog,
|
||||
blurSignInDialog,
|
||||
hideSignInDialog,
|
||||
updateStatus,
|
||||
} = authActions;
|
||||
const { fetchAssetSuccess } = assetActions;
|
||||
|
||||
class EmbedContainer extends React.Component {
|
||||
static contextTypes = {
|
||||
pym: PropTypes.object,
|
||||
@@ -42,7 +37,7 @@ class EmbedContainer extends React.Component {
|
||||
subscriptions = [];
|
||||
|
||||
subscribeToUpdates(props = this.props) {
|
||||
if (props.auth.loggedIn) {
|
||||
if (props.currentUser) {
|
||||
const newSubscriptions = [
|
||||
{
|
||||
document: USER_BANNED_SUBSCRIPTION,
|
||||
@@ -80,7 +75,7 @@ class EmbedContainer extends React.Component {
|
||||
props.data.subscribeToMore({
|
||||
document: s.document,
|
||||
variables: {
|
||||
user_id: props.auth.user.id,
|
||||
user_id: props.currentUser.id,
|
||||
},
|
||||
updateQuery: s.updateQuery,
|
||||
})
|
||||
@@ -107,7 +102,7 @@ class EmbedContainer extends React.Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.auth.loggedIn !== nextProps.auth.loggedIn) {
|
||||
if (this.props.currentUser !== nextProps.currentUser) {
|
||||
// Refetch after login/logout.
|
||||
this.props.data.refetch();
|
||||
this.resubscribe(nextProps);
|
||||
@@ -138,7 +133,23 @@ class EmbedContainer extends React.Component {
|
||||
if (!this.props.root.asset) {
|
||||
return <Spinner />;
|
||||
}
|
||||
return <Embed {...this.props} />;
|
||||
return (
|
||||
<Embed
|
||||
setActiveTab={this.props.setActiveTab}
|
||||
currentUser={this.props.currentUser}
|
||||
blurSignInDialog={this.props.blurSignInDialog}
|
||||
focusSignInDialog={this.props.focusSignInDialog}
|
||||
hideSignInDialog={this.props.hideSignInDialog}
|
||||
router={this.props.router}
|
||||
commentId={this.props.commentId}
|
||||
root={this.props.root}
|
||||
activeTab={this.props.activeTab}
|
||||
data={this.props.data}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
signInDialogFocus={this.props.signInDialogFocus}
|
||||
parentUrl={this.props.parentUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,21 +266,46 @@ const EMBED_QUERY = gql`
|
||||
`;
|
||||
|
||||
export const withEmbedQuery = withQuery(EMBED_QUERY, {
|
||||
options: ({ auth, commentId, assetId, assetUrl, sortBy, sortOrder }) => ({
|
||||
options: ({
|
||||
currentUser,
|
||||
commentId,
|
||||
assetId,
|
||||
assetUrl,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
}) => ({
|
||||
variables: {
|
||||
assetId,
|
||||
assetUrl,
|
||||
commentId,
|
||||
hasComment: commentId !== '',
|
||||
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
|
||||
excludeIgnored: Boolean(currentUser && currentUser.id),
|
||||
sortBy,
|
||||
sortOrder,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
EmbedContainer.propTypes = {
|
||||
setActiveTab: PropTypes.func,
|
||||
currentUser: PropTypes.object,
|
||||
blurSignInDialog: PropTypes.func,
|
||||
focusSignInDialog: PropTypes.func,
|
||||
hideSignInDialog: PropTypes.func,
|
||||
router: PropTypes.object,
|
||||
commentId: PropTypes.string,
|
||||
root: PropTypes.object,
|
||||
activeTab: PropTypes.string,
|
||||
parentUrl: PropTypes.string,
|
||||
data: PropTypes.object,
|
||||
fetchAssetSuccess: PropTypes.func,
|
||||
showSignInDialog: PropTypes.bool,
|
||||
signInDialogFocus: PropTypes.bool,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth,
|
||||
currentUser: state.auth.user,
|
||||
checkedInitialLogin: state.auth.checkedInitialLogin,
|
||||
commentId: state.stream.commentId,
|
||||
assetId: state.stream.assetId,
|
||||
assetUrl: state.stream.assetUrl,
|
||||
@@ -277,13 +313,14 @@ const mapStateToProps = state => ({
|
||||
config: state.config,
|
||||
sortOrder: state.stream.sortOrder,
|
||||
sortBy: state.stream.sortBy,
|
||||
showSignInDialog: state.login.showSignInDialog,
|
||||
signInDialogFocus: state.login.signInDialogFocus,
|
||||
parentUrl: state.login.parentUrl,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
logout,
|
||||
checkLogin,
|
||||
setActiveTab,
|
||||
fetchAssetSuccess,
|
||||
notify,
|
||||
@@ -297,6 +334,6 @@ const mapDispatchToProps = dispatch =>
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
branch(props => !props.auth.checkedInitialLogin, renderComponent(Spinner)),
|
||||
branch(props => !props.checkedInitialLogin, renderComponent(Spinner)),
|
||||
withEmbedQuery
|
||||
)(EmbedContainer);
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import React from 'react';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
export const LoginContainer = () => <Slot fill="login" />;
|
||||
@@ -1,59 +1,22 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import Embed from './containers/Embed';
|
||||
|
||||
import {
|
||||
checkLogin,
|
||||
handleAuthToken,
|
||||
logout,
|
||||
} from 'coral-embed-stream/src/actions/auth';
|
||||
import graphqlExtension from './graphql';
|
||||
import { addExternalConfig } from 'coral-embed-stream/src/actions/config';
|
||||
import { createContext } from 'coral-framework/services/bootstrap';
|
||||
import AppRouter from './AppRouter';
|
||||
import reducers from './reducers';
|
||||
import TalkProvider from 'coral-framework/components/TalkProvider';
|
||||
import pluginsConfig from 'pluginsConfig';
|
||||
|
||||
// TODO: move init code into `bootstrap` service after auth has been refactored.
|
||||
function preInit({ store, pym, inIframe }) {
|
||||
// TODO: This is popup specific code and needs to be refactored.
|
||||
if (!inIframe) {
|
||||
store.dispatch(addExternalConfig({}));
|
||||
store.dispatch(checkLogin());
|
||||
return;
|
||||
}
|
||||
|
||||
pym.onMessage('login', token => {
|
||||
if (token) {
|
||||
store.dispatch(handleAuthToken(token));
|
||||
}
|
||||
store.dispatch(checkLogin());
|
||||
});
|
||||
|
||||
pym.onMessage('logout', () => {
|
||||
store.dispatch(logout());
|
||||
});
|
||||
|
||||
return new Promise(resolve => {
|
||||
pym.sendMessage('getConfig');
|
||||
pym.onMessage('config', config => {
|
||||
store.dispatch(addExternalConfig(JSON.parse(config)));
|
||||
store.dispatch(checkLogin());
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const context = await createContext({
|
||||
reducers,
|
||||
graphqlExtension,
|
||||
pluginsConfig,
|
||||
preInit,
|
||||
});
|
||||
render(
|
||||
<TalkProvider {...context}>
|
||||
<AppRouter />
|
||||
<Embed />
|
||||
</TalkProvider>,
|
||||
document.querySelector('#talk-embed-stream-container')
|
||||
);
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
import * as actions from '../constants/auth';
|
||||
import pym from 'coral-framework/services/pym';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
showSignInDialog: false,
|
||||
signInDialogFocus: false,
|
||||
showCreateUsernameDialog: false,
|
||||
checkedInitialLogin: false,
|
||||
view: 'SIGNIN',
|
||||
error: null,
|
||||
passwordRequestSuccess: null,
|
||||
passwordRequestFailure: null,
|
||||
emailVerificationFailure: false,
|
||||
emailVerificationLoading: false,
|
||||
emailVerificationSuccess: false,
|
||||
successSignUp: false,
|
||||
fromSignUp: false,
|
||||
requireEmailConfirmation: false,
|
||||
redirectUri: pym.parentUrl || location.href,
|
||||
};
|
||||
|
||||
const purge = user => {
|
||||
const {settings, ...userData} = user; // eslint-disable-line
|
||||
return userData;
|
||||
};
|
||||
|
||||
export default function auth(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FOCUS_SIGNIN_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
signInDialogFocus: true,
|
||||
};
|
||||
case actions.BLUR_SIGNIN_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
signInDialogFocus: false,
|
||||
};
|
||||
case actions.SHOW_SIGNIN_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
showSignInDialog: true,
|
||||
signInDialogFocus: true,
|
||||
};
|
||||
case actions.RESET_SIGNIN_DIALOG:
|
||||
case actions.HIDE_SIGNIN_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
showSignInDialog: false,
|
||||
signInDialogFocus: false,
|
||||
view: 'SIGNIN',
|
||||
error: null,
|
||||
passwordRequestFailure: null,
|
||||
passwordRequestSuccess: null,
|
||||
emailVerificationFailure: false,
|
||||
emailVerificationSuccess: false,
|
||||
emailVerificationLoading: false,
|
||||
successSignUp: false,
|
||||
};
|
||||
case actions.SHOW_CREATEUSERNAME_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
showCreateUsernameDialog: true,
|
||||
};
|
||||
case actions.HIDE_CREATEUSERNAME_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
showCreateUsernameDialog: false,
|
||||
};
|
||||
case actions.CREATE_USERNAME_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
showCreateUsernameDialog: false,
|
||||
error: null,
|
||||
};
|
||||
case actions.CREATE_USERNAME_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
};
|
||||
case actions.CHANGE_VIEW:
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
view: action.view,
|
||||
};
|
||||
case actions.CLEAN_STATE:
|
||||
return initialState;
|
||||
case actions.FETCH_SIGNIN_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
email: action.email,
|
||||
isLoading: true,
|
||||
};
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
checkedInitialLogin: true,
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
};
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
checkedInitialLogin: true,
|
||||
loggedIn: true,
|
||||
user: purge(action.user),
|
||||
};
|
||||
case actions.FETCH_SIGNIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loggedIn: true,
|
||||
user: purge(action.user),
|
||||
};
|
||||
case actions.FETCH_SIGNIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
error: action.error,
|
||||
user: null,
|
||||
view:
|
||||
action.error.translation_key === 'EMAIL_NOT_VERIFIED'
|
||||
? 'RESEND_VERIFICATION'
|
||||
: state.view,
|
||||
};
|
||||
case actions.FETCH_SIGNUP_FACEBOOK_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
fromSignUp: true,
|
||||
};
|
||||
case actions.FETCH_SIGNIN_FACEBOOK_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
fromSignUp: false,
|
||||
};
|
||||
case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loggedIn: true,
|
||||
user: purge(action.user),
|
||||
};
|
||||
case actions.FETCH_SIGNIN_FACEBOOK_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
user: null,
|
||||
};
|
||||
case actions.FETCH_SIGNUP_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
isLoading: true,
|
||||
};
|
||||
case actions.FETCH_SIGNUP_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
isLoading: false,
|
||||
};
|
||||
case actions.FETCH_SIGNUP_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
successSignUp: true,
|
||||
};
|
||||
case actions.LOGOUT:
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
isLoading: false,
|
||||
loggedIn: false,
|
||||
};
|
||||
case actions.INVALID_FORM:
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
};
|
||||
case actions.VALID_FORM:
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
passwordRequestFailure: null,
|
||||
passwordRequestSuccess:
|
||||
'If you have a registered account, a password reset link was sent to that email',
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
passwordRequestFailure:
|
||||
'There was an error sending your password reset email. Please try again soon!',
|
||||
passwordRequestSuccess: null,
|
||||
};
|
||||
case actions.UPDATE_USERNAME:
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
username: action.username,
|
||||
lowercaseUsername: action.username.toLowerCase(),
|
||||
},
|
||||
};
|
||||
case actions.VERIFY_EMAIL_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
emailVerificationFailure: action.error,
|
||||
emailVerificationLoading: false,
|
||||
};
|
||||
case actions.VERIFY_EMAIL_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
emailVerificationLoading: true,
|
||||
};
|
||||
case actions.VERIFY_EMAIL_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
emailVerificationSuccess: true,
|
||||
emailVerificationLoading: false,
|
||||
};
|
||||
case actions.SET_REQUIRE_EMAIL_VERIFICATION:
|
||||
return {
|
||||
...state,
|
||||
requireEmailConfirmation: action.required,
|
||||
};
|
||||
case actions.SET_REDIRECT_URI:
|
||||
return {
|
||||
...state,
|
||||
redirectUri: action.uri,
|
||||
};
|
||||
case actions.UPDATE_STATUS: {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
status: merge({}, state.user.status, action.status),
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
import auth from './auth';
|
||||
import login from './login';
|
||||
import asset from './asset';
|
||||
import embed from './embed';
|
||||
import config from './config';
|
||||
import configure from './configure';
|
||||
import stream from './stream';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
login,
|
||||
asset,
|
||||
embed,
|
||||
config,
|
||||
configure,
|
||||
stream,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as actions from '../constants/login';
|
||||
import pym from 'coral-framework/services/pym';
|
||||
|
||||
const initialState = {
|
||||
parentUrl: pym.parentUrl || location.href,
|
||||
showSignInDialog: false,
|
||||
signInDialogFocus: false,
|
||||
};
|
||||
|
||||
export default function login(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FOCUS_SIGNIN_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
signInDialogFocus: true,
|
||||
};
|
||||
case actions.BLUR_SIGNIN_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
signInDialogFocus: false,
|
||||
};
|
||||
case actions.SHOW_SIGNIN_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
showSignInDialog: true,
|
||||
signInDialogFocus: true,
|
||||
};
|
||||
|
||||
case actions.HIDE_SIGNIN_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
showSignInDialog: false,
|
||||
signInDialogFocus: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as actions from '../constants/stream';
|
||||
import * as authActions from '../constants/auth';
|
||||
import * as authActions from 'coral-framework/constants/auth';
|
||||
|
||||
function getQueryVariable(variable) {
|
||||
let query = window.location.search.substring(1);
|
||||
|
||||
@@ -10,12 +10,7 @@ import NotLoggedIn from '../components/NotLoggedIn';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import CommentHistory from '../components/CommentHistory';
|
||||
|
||||
// TODO: Auth logic needs refactoring.
|
||||
import {
|
||||
showSignInDialog,
|
||||
checkLogin,
|
||||
} from 'coral-embed-stream/src/actions/auth';
|
||||
|
||||
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
|
||||
import { appendNewNodes } from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
@@ -24,7 +19,7 @@ import t from 'coral-framework/services/i18n';
|
||||
|
||||
class ProfileContainer extends Component {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.auth.loggedIn && nextProps.auth.loggedIn) {
|
||||
if (!this.props.currentUser && nextProps.currentUser) {
|
||||
// Refetch after login.
|
||||
this.props.data.refetch();
|
||||
}
|
||||
@@ -55,13 +50,7 @@ class ProfileContainer extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
auth,
|
||||
auth: { user: authUser },
|
||||
showSignInDialog,
|
||||
root,
|
||||
data,
|
||||
} = this.props;
|
||||
const { currentUser, showSignInDialog, root, data } = this.props;
|
||||
const { me } = this.props.root;
|
||||
const loading = this.props.data.loading;
|
||||
|
||||
@@ -69,7 +58,7 @@ class ProfileContainer extends Component {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (!auth.loggedIn) {
|
||||
if (!currentUser) {
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
|
||||
}
|
||||
|
||||
@@ -77,7 +66,7 @@ class ProfileContainer extends Component {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const localProfile = authUser.profiles.find(p => p.provider === 'local');
|
||||
const localProfile = currentUser.profiles.find(p => p.provider === 'local');
|
||||
const emailAddress = localProfile && localProfile.id;
|
||||
|
||||
return (
|
||||
@@ -168,15 +157,20 @@ const withProfileQuery = withQuery(
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
${CommentFragment}
|
||||
`
|
||||
`,
|
||||
{
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth,
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ showSignInDialog, checkLogin }, dispatch);
|
||||
bindActionCreators({ showSignInDialog }, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
|
||||
@@ -59,7 +59,7 @@ class Stream extends React.Component {
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
loadNewReplies,
|
||||
auth: { user },
|
||||
currentUser,
|
||||
emit,
|
||||
viewAllComments,
|
||||
} = this.props;
|
||||
@@ -111,7 +111,7 @@ class Stream extends React.Component {
|
||||
disableReply={!open}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
currentUser={currentUser}
|
||||
highlighted={comment.id}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
@@ -150,7 +150,7 @@ class Stream extends React.Component {
|
||||
setActiveStreamTab,
|
||||
loadNewReplies,
|
||||
loadMoreComments,
|
||||
auth: { user },
|
||||
currentUser,
|
||||
emit,
|
||||
sortOrder,
|
||||
sortBy,
|
||||
@@ -200,7 +200,7 @@ class Stream extends React.Component {
|
||||
notify={notify}
|
||||
disableReply={asset.isClosed}
|
||||
postComment={postComment}
|
||||
currentUser={user}
|
||||
currentUser={currentUser}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
loadMore={loadMoreComments}
|
||||
@@ -230,21 +230,23 @@ class Stream extends React.Component {
|
||||
postComment,
|
||||
notify,
|
||||
updateItem,
|
||||
auth: { loggedIn, user },
|
||||
currentUser,
|
||||
} = this.props;
|
||||
const { keepCommentBox } = this.state;
|
||||
const open = !asset.isClosed;
|
||||
|
||||
const banned = get(user, 'status.banned.status');
|
||||
const suspensionUntil = get(user, 'status.suspension.until');
|
||||
const rejectedUsername = get(user, 'status.username.status') === 'REJECTED';
|
||||
const changedUsername = get(user, 'status.username.status') === 'CHANGED';
|
||||
const banned = get(currentUser, 'status.banned.status');
|
||||
const suspensionUntil = get(currentUser, 'status.suspension.until');
|
||||
const rejectedUsername =
|
||||
get(currentUser, 'status.username.status') === 'REJECTED';
|
||||
const changedUsername =
|
||||
get(currentUser, 'status.username.status') === 'CHANGED';
|
||||
|
||||
const temporarilySuspended =
|
||||
user && suspensionUntil && new Date(suspensionUntil) > new Date();
|
||||
currentUser && suspensionUntil && new Date(suspensionUntil) > new Date();
|
||||
|
||||
const showCommentBox =
|
||||
loggedIn &&
|
||||
currentUser &&
|
||||
((!banned &&
|
||||
!temporarilySuspended &&
|
||||
!rejectedUsername &&
|
||||
@@ -289,7 +291,8 @@ class Stream extends React.Component {
|
||||
</RestrictedMessageBox>
|
||||
)}
|
||||
{changedUsername && <ChangedUsername />}
|
||||
{!banned && rejectedUsername && <ChangeUsername user={user} />}
|
||||
{!banned &&
|
||||
rejectedUsername && <ChangeUsername user={currentUser} />}
|
||||
{banned && <BannedAccount />}
|
||||
{showCommentBox && (
|
||||
<CommentBox
|
||||
@@ -300,7 +303,7 @@ class Stream extends React.Component {
|
||||
assetId={asset.id}
|
||||
premod={asset.settings.moderation}
|
||||
isReply={false}
|
||||
currentUser={user}
|
||||
currentUser={currentUser}
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
/>
|
||||
@@ -312,10 +315,10 @@ class Stream extends React.Component {
|
||||
|
||||
<Slot fill="stream" queryData={slotQueryData} {...slotProps} />
|
||||
|
||||
{loggedIn && (
|
||||
{currentUser && (
|
||||
<ModerationLink
|
||||
assetId={asset.id}
|
||||
isAdmin={can(user, 'MODERATE_COMMENTS')}
|
||||
isAdmin={can(currentUser, 'MODERATE_COMMENTS')}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -342,12 +345,11 @@ Stream.propTypes = {
|
||||
deleteAction: PropTypes.func,
|
||||
showSignInDialog: PropTypes.func,
|
||||
loadNewReplies: PropTypes.func,
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
emit: PropTypes.func,
|
||||
sortOrder: PropTypes.string,
|
||||
sortBy: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
editName: PropTypes.func,
|
||||
appendItemArray: PropTypes.func,
|
||||
updateItem: PropTypes.func,
|
||||
viewAllComments: PropTypes.func,
|
||||
|
||||
@@ -4,7 +4,10 @@ import Comment from '../components/Comment';
|
||||
import { withFragments } from 'coral-framework/hocs';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
import { withSetCommentStatus } from 'coral-framework/graphql/mutations';
|
||||
import { THREADING_LEVEL } from '../../../constants/stream';
|
||||
import {
|
||||
THREADING_LEVEL,
|
||||
REPLY_COMMENTS_LOAD_DEPTH,
|
||||
} from '../../../constants/stream';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import { nest } from '../../../graphql/utils';
|
||||
|
||||
@@ -118,7 +121,7 @@ const withCommentFragments = withFragments({
|
||||
...CoralEmbedStream_Comment_SingleComment
|
||||
${nest(
|
||||
`
|
||||
replies(query: {limit: 3, excludeIgnored: $excludeIgnored}) {
|
||||
replies(query: {limit: ${REPLY_COMMENTS_LOAD_DEPTH}, excludeIgnored: $excludeIgnored}) {
|
||||
nodes {
|
||||
...CoralEmbedStream_Comment_SingleComment
|
||||
...nest
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gql, compose } from 'react-apollo';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import {
|
||||
ADDTL_COMMENTS_ON_LOAD_MORE,
|
||||
ASSET_COMMENTS_LOAD_DEPTH,
|
||||
THREADING_LEVEL,
|
||||
} from '../../../constants/stream';
|
||||
import {
|
||||
@@ -14,8 +16,8 @@ import {
|
||||
withEditComment,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
|
||||
import * as authActions from 'coral-embed-stream/src/actions/auth';
|
||||
import * as notificationActions from 'coral-framework/actions/notification';
|
||||
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import {
|
||||
setActiveReplyBox,
|
||||
setActiveTab,
|
||||
@@ -40,9 +42,6 @@ import {
|
||||
} from '../../../graphql/utils';
|
||||
import StreamError from '../components/StreamError';
|
||||
|
||||
const { showSignInDialog, editName } = authActions;
|
||||
const { notify } = notificationActions;
|
||||
|
||||
class StreamContainer extends React.Component {
|
||||
commentsAddedSubscription = null;
|
||||
commentsEditedSubscription = null;
|
||||
@@ -60,8 +59,8 @@ class StreamContainer extends React.Component {
|
||||
// Ignore mutations from me.
|
||||
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
|
||||
if (
|
||||
this.props.auth.user &&
|
||||
commentEdited.user.id === this.props.auth.user.id
|
||||
this.props.currentUser &&
|
||||
commentEdited.user.id === this.props.currentUser.id
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
@@ -92,8 +91,8 @@ class StreamContainer extends React.Component {
|
||||
// Ignore mutations from me.
|
||||
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
|
||||
if (
|
||||
this.props.auth.user &&
|
||||
commentAdded.user.id === this.props.auth.user.id
|
||||
this.props.currentUser &&
|
||||
commentAdded.user.id === this.props.currentUser.id
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
@@ -204,8 +203,8 @@ class StreamContainer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
userIsDegraged({ auth: { user } } = this.props) {
|
||||
return !can(user, 'INTERACT_WITH_COMMUNITY');
|
||||
userIsDegraged({ currentUser } = this.props) {
|
||||
return !can(currentUser, 'INTERACT_WITH_COMMUNITY');
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -225,8 +224,28 @@ class StreamContainer extends React.Component {
|
||||
|
||||
return (
|
||||
<Stream
|
||||
{...this.props}
|
||||
loadMore={this.loadMore}
|
||||
asset={this.props.asset}
|
||||
activeStreamTab={this.props.activeStreamTab}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
activeReplyBox={this.props.activeReplyBox}
|
||||
setActiveReplyBox={this.props.setActiveReplyBox}
|
||||
commentClassNames={this.props.commentClassNames}
|
||||
setActiveStreamTab={this.props.setActiveStreamTab}
|
||||
postFlag={this.props.postFlag}
|
||||
postDontAgree={this.props.postDontAgree}
|
||||
deleteAction={this.props.deleteAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
currentUser={this.props.currentUser}
|
||||
emit={this.props.emit}
|
||||
sortOrder={this.props.sortOrder}
|
||||
sortBy={this.props.sortBy}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
viewAllComments={this.props.viewAllComments}
|
||||
notify={this.props.notify}
|
||||
postComment={this.props.postComment}
|
||||
editComment={this.props.editComment}
|
||||
loadMoreComments={this.loadMoreComments}
|
||||
loadNewReplies={this.loadNewReplies}
|
||||
userIsDegraged={this.userIsDegraged()}
|
||||
@@ -236,6 +255,33 @@ class StreamContainer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
StreamContainer.propTypes = {
|
||||
asset: PropTypes.object,
|
||||
activeStreamTab: PropTypes.string,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
activeReplyBox: PropTypes.string,
|
||||
setActiveReplyBox: PropTypes.func,
|
||||
commentClassNames: PropTypes.array,
|
||||
setActiveStreamTab: PropTypes.func,
|
||||
postFlag: PropTypes.func,
|
||||
postDontAgree: PropTypes.func,
|
||||
deleteAction: PropTypes.func,
|
||||
showSignInDialog: PropTypes.func,
|
||||
currentUser: PropTypes.object,
|
||||
emit: PropTypes.func,
|
||||
sortOrder: PropTypes.string,
|
||||
sortBy: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
appendItemArray: PropTypes.func,
|
||||
updateItem: PropTypes.func,
|
||||
viewAllComments: PropTypes.func,
|
||||
notify: PropTypes.func.isRequired,
|
||||
postComment: PropTypes.func.isRequired,
|
||||
editComment: PropTypes.func,
|
||||
previousTab: PropTypes.string,
|
||||
};
|
||||
|
||||
const commentFragment = gql`
|
||||
fragment CoralEmbedStream_Stream_comment on Comment {
|
||||
id
|
||||
@@ -379,7 +425,7 @@ const fragments = {
|
||||
requireEmailConfirmation
|
||||
}
|
||||
totalCommentCount @skip(if: $hasComment)
|
||||
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
|
||||
comments(query: {limit: ${ASSET_COMMENTS_LOAD_DEPTH}, excludeIgnored: $excludeIgnored, sortOrder: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
|
||||
nodes {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
}
|
||||
@@ -396,7 +442,7 @@ const fragments = {
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth,
|
||||
currentUser: state.auth.user,
|
||||
activeReplyBox: state.stream.activeReplyBox,
|
||||
commentId: state.stream.commentId,
|
||||
assetId: state.stream.assetId,
|
||||
@@ -417,7 +463,6 @@ const mapDispatchToProps = dispatch =>
|
||||
showSignInDialog,
|
||||
notify,
|
||||
setActiveReplyBox,
|
||||
editName,
|
||||
viewAllComments,
|
||||
setActiveStreamTab: setActiveTab,
|
||||
},
|
||||
|
||||
@@ -72,17 +72,6 @@ body {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Coral sign in button */
|
||||
|
||||
#coralSignInButton {
|
||||
background-color: #2a2a2a;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
#coralSignInButton:hover {
|
||||
background-color: #767676;
|
||||
}
|
||||
|
||||
/* Info Box Styles */
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import * as actions from '../constants/auth';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
function cleanAuthData(localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
|
||||
export const checkLogin = () => (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, pym, localStorage }
|
||||
) => {
|
||||
dispatch(checkLoginRequest());
|
||||
rest('/auth')
|
||||
.then(result => {
|
||||
if (!result.user) {
|
||||
if (localStorage) {
|
||||
cleanAuthData(localStorage);
|
||||
}
|
||||
dispatch(checkLoginSuccess(null));
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch(checkLoginSuccess(result.user));
|
||||
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status && error.status === 401 && localStorage) {
|
||||
// Unauthorized.
|
||||
cleanAuthData(localStorage);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
dispatch(checkLoginFailure(error));
|
||||
});
|
||||
};
|
||||
|
||||
const checkLoginRequest = () => ({ type: actions.CHECK_LOGIN_REQUEST });
|
||||
|
||||
const checkLoginFailure = error => ({
|
||||
type: actions.CHECK_LOGIN_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
const checkLoginSuccess = user => ({
|
||||
type: actions.CHECK_LOGIN_SUCCESS,
|
||||
user,
|
||||
});
|
||||
|
||||
export const setAuthToken = token => (dispatch, _, { localStorage }) => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
dispatch(checkLogin());
|
||||
};
|
||||
|
||||
export const handleSuccessfulLogin = (user, token) => (
|
||||
dispatch,
|
||||
_,
|
||||
{ client, localStorage }
|
||||
) => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({
|
||||
type: actions.HANDLE_SUCCESSFUL_LOGIN,
|
||||
user,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
export const logout = () => async (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, pym, localStorage }
|
||||
) => {
|
||||
await rest('/auth', { method: 'DELETE' });
|
||||
|
||||
if (localStorage) {
|
||||
cleanAuthData(localStorage);
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({ type: actions.LOGOUT });
|
||||
pym.sendMessage('coral-auth-changed');
|
||||
};
|
||||
|
||||
export const updateStatus = status => ({
|
||||
type: actions.UPDATE_STATUS,
|
||||
status,
|
||||
});
|
||||
|
||||
export const updateUsername = username => ({
|
||||
type: actions.UPDATE_USERNAME,
|
||||
username,
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MERGE_CONFIG } from '../constants/config';
|
||||
|
||||
export const mergeConfig = config => ({
|
||||
type: MERGE_CONFIG,
|
||||
config,
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactRecaptcha from 'react-recaptcha';
|
||||
|
||||
class Recaptcha extends React.Component {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
};
|
||||
|
||||
ref = null;
|
||||
|
||||
handleRef = ref => {
|
||||
this.ref = ref;
|
||||
};
|
||||
|
||||
reset = () => this.ref.reset();
|
||||
|
||||
getSiteKey() {
|
||||
// This should be fine because it's static and will never change.
|
||||
// Prefer this to connect HOC because wie expose the instance method
|
||||
// `reset`
|
||||
return this.context.store.getState().config.static.TALK_RECAPTCHA_PUBLIC;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ReactRecaptcha
|
||||
ref={this.handleRef}
|
||||
sitekey={this.getSiteKey()}
|
||||
render={this.props.render}
|
||||
theme={this.props.theme}
|
||||
onloadCallback={this.props.onLoad}
|
||||
verifyCallback={this.props.onVerify}
|
||||
size={this.props.size}
|
||||
className={this.props.className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Recaptcha.defaultProps = {
|
||||
render: 'explicit',
|
||||
theme: 'light',
|
||||
size: 'normal',
|
||||
};
|
||||
|
||||
Recaptcha.propTypes = {
|
||||
onLoad: PropTypes.func,
|
||||
onVerify: PropTypes.func.isRequired,
|
||||
theme: PropTypes.string,
|
||||
render: PropTypes.string,
|
||||
size: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Recaptcha;
|
||||
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import get from 'lodash/get';
|
||||
import { getShallowChanges } from 'coral-framework/utils';
|
||||
|
||||
const emptyConfig = {};
|
||||
@@ -68,7 +69,7 @@ class Slot extends React.Component {
|
||||
} = this.props;
|
||||
const { plugins } = this.context;
|
||||
let children = this.getChildren();
|
||||
const pluginConfig = reduxState.config.pluginConfig || emptyConfig;
|
||||
const pluginConfig = get(reduxState, 'config.pluginConfig') || emptyConfig;
|
||||
if (children.length === 0 && DefaultComponent) {
|
||||
const props = plugins.getSlotComponentProps(
|
||||
DefaultComponent,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const prefix = `TALK_FRAMEWORK`;
|
||||
|
||||
export const CHECK_LOGIN_REQUEST = `${prefix}_CHECK_LOGIN_REQUEST`;
|
||||
export const CHECK_LOGIN_SUCCESS = `${prefix}_CHECK_LOGIN_SUCCESS`;
|
||||
export const CHECK_LOGIN_FAILURE = `${prefix}_CHECK_LOGIN_FAILURE`;
|
||||
|
||||
export const LOGOUT = `${prefix}_LOGOUT`;
|
||||
export const HANDLE_SUCCESSFUL_LOGIN = `${prefix}_HANDLE_SUCCESSFUL_LOGIN`;
|
||||
|
||||
export const UPDATE_STATUS = '${prefix}_UPDATE_STATUS';
|
||||
export const UPDATE_USERNAME = '${prefix}_UPDATE_USERNAME';
|
||||
@@ -0,0 +1,3 @@
|
||||
const prefix = `TALK_FRAMEWORK`;
|
||||
|
||||
export const MERGE_CONFIG = `${prefix}_MERGE_CONFIG`;
|
||||
@@ -6,3 +6,10 @@ export { default as withEmit } from './withEmit';
|
||||
export { default as excludeIf } from './excludeIf';
|
||||
export { default as connect } from './connect';
|
||||
export { default as withMergedSettings } from './withMergedSettings';
|
||||
export { default as withSignIn } from './withSignIn';
|
||||
export { default as withSignUp } from './withSignUp';
|
||||
export { default as withForgotPassword } from './withForgotPassword';
|
||||
export { default as withSetUsername } from './withSetUsername';
|
||||
export {
|
||||
default as withResendEmailConfirmation,
|
||||
} from './withResendEmailConfirmation';
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translateError } from '../utils';
|
||||
|
||||
/**
|
||||
* WithForgotPassword provides properties
|
||||
* `forgotPasssword`,
|
||||
* `loading`,
|
||||
* `errorMessage`,
|
||||
* `success`.
|
||||
*/
|
||||
export default hoistStatics(WrappedComponent => {
|
||||
class WithForgotPassword extends React.Component {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
rest: PropTypes.func,
|
||||
pym: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
success: false,
|
||||
};
|
||||
|
||||
forgotPassword = (email, redirectUri) => {
|
||||
if (!redirectUri) {
|
||||
redirectUri = this.context.pym.parentUrl || location.href;
|
||||
}
|
||||
const { rest } = this.context;
|
||||
this.setState({ loading: true, error: null, success: false });
|
||||
|
||||
rest('/account/password/reset', {
|
||||
method: 'POST',
|
||||
body: { email, loc: redirectUri },
|
||||
})
|
||||
.then(() => {
|
||||
this.setState({ loading: false, error: null, success: true });
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
this.setState({ loading: false, error });
|
||||
});
|
||||
};
|
||||
|
||||
getErrorMessage() {
|
||||
if (!this.state.error) {
|
||||
return null;
|
||||
}
|
||||
return translateError(this.state.error);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
forgotPassword={this.forgotPassword}
|
||||
success={this.state.success}
|
||||
loading={this.state.loading}
|
||||
errorMessage={this.getErrorMessage()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithForgotPassword;
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translateError } from '../utils';
|
||||
|
||||
/**
|
||||
* WithResendEmailConfirmaton provides properties
|
||||
* `resendEmailConfirmation`,
|
||||
* `loading`,
|
||||
* `errorMessage`,
|
||||
* `success`.
|
||||
*/
|
||||
export default hoistStatics(WrappedComponent => {
|
||||
class WithResendEmailConfirmaton extends React.Component {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
rest: PropTypes.func,
|
||||
pym: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
success: false,
|
||||
};
|
||||
|
||||
resendEmailConfirmation = (email, redirectUri) => {
|
||||
if (!redirectUri) {
|
||||
redirectUri = this.context.pym.parentUrl || location.href;
|
||||
}
|
||||
const { rest } = this.context;
|
||||
this.setState({ loading: true, error: null, success: false });
|
||||
|
||||
rest('/users/resend-verify', {
|
||||
method: 'POST',
|
||||
body: { email },
|
||||
headers: { 'X-Pym-Url': redirectUri },
|
||||
})
|
||||
.then(() => {
|
||||
this.setState({ loading: false, error: null, success: true });
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
this.setState({ loading: false, error });
|
||||
});
|
||||
};
|
||||
|
||||
getErrorMessage() {
|
||||
if (!this.state.error) {
|
||||
return null;
|
||||
}
|
||||
return translateError(this.state.error);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
resendEmailConfirmation={this.resendEmailConfirmation}
|
||||
success={this.state.success}
|
||||
loading={this.state.loading}
|
||||
errorMessage={this.getErrorMessage()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithResendEmailConfirmaton;
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getErrorMessages } from '../utils';
|
||||
import validate from '../helpers/validate';
|
||||
import errorMsg from 'coral-framework/helpers/error';
|
||||
import t from '../services/i18n';
|
||||
import { withSetUsername as withSetUsernameMutation } from 'coral-framework/graphql/mutations';
|
||||
import { updateUsername, updateStatus } from '../actions/auth';
|
||||
import { compose } from 'recompose';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import get from 'lodash/get';
|
||||
|
||||
/**
|
||||
* withSetUsername provides properties
|
||||
* `setUsername`,
|
||||
* `loading`,
|
||||
* `errorMessage`,
|
||||
* `requireEmailVerification`,
|
||||
* `success`,
|
||||
* `validateUsername`.
|
||||
*/
|
||||
const withSetUsername = hoistStatics(WrappedComponent => {
|
||||
class WithSetUsername extends React.Component {
|
||||
static propTypes = {
|
||||
setUsername: PropTypes.func.isRequired,
|
||||
currentUserId: PropTypes.string,
|
||||
updateUsername: PropTypes.func.isRequired,
|
||||
updateStatus: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
success: false,
|
||||
};
|
||||
|
||||
validateUsername = value => {
|
||||
if (!value) {
|
||||
return t('error.required_field');
|
||||
}
|
||||
return validate.username(value) ? null : errorMsg.username;
|
||||
};
|
||||
|
||||
setUsername = async username => {
|
||||
if (!this.props.currentUserId) {
|
||||
throw new Error('User not logged in');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.props.setUsername(this.props.currentUserId, username);
|
||||
this.props.updateUsername(username);
|
||||
this.props.updateStatus({ username: { status: 'SET' } });
|
||||
this.setState({ success: true, loading: false, error: null });
|
||||
} catch (error) {
|
||||
if (!error.status || error.status !== 401) {
|
||||
console.error(error);
|
||||
}
|
||||
const changeSet = { success: false, loading: false, error };
|
||||
this.setState(changeSet);
|
||||
}
|
||||
};
|
||||
|
||||
getErrorMessage() {
|
||||
if (!this.state.error) {
|
||||
return null;
|
||||
}
|
||||
return getErrorMessages(this.state.error).join(', ');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
setUsername={this.setUsername}
|
||||
loading={this.state.loading}
|
||||
errorMessage={this.getErrorMessage()}
|
||||
success={this.state.success}
|
||||
validateUsername={this.validateUsername}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithSetUsername;
|
||||
});
|
||||
|
||||
const mapStateToProps = ({ auth }) => ({
|
||||
currentUserId: get(auth, 'user.id'),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ updateUsername, updateStatus }, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetUsernameMutation,
|
||||
withSetUsername
|
||||
);
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import PropTypes from 'prop-types';
|
||||
import { handleSuccessfulLogin } from '../actions/auth';
|
||||
import { translateError } from '../utils';
|
||||
import { t } from '../services/i18n';
|
||||
|
||||
/**
|
||||
* WithSignIn provides properties
|
||||
* `signIn`
|
||||
* `loading`
|
||||
* `errorMessage`
|
||||
* `requireRecaptcha`
|
||||
* `requireEmailConfirmation`
|
||||
* 'success'
|
||||
*/
|
||||
export default hoistStatics(WrappedComponent => {
|
||||
class WithSignIn extends React.Component {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
rest: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
success: false,
|
||||
requireRecaptcha: false,
|
||||
requireEmailConfirmation: false,
|
||||
};
|
||||
|
||||
signIn = (email, password, recaptchaResponse) => {
|
||||
const { store, rest } = this.context;
|
||||
const params = {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
};
|
||||
|
||||
if (recaptchaResponse) {
|
||||
params.headers = {
|
||||
'X-Recaptcha-Response': recaptchaResponse,
|
||||
};
|
||||
}
|
||||
|
||||
rest('/auth/local', params)
|
||||
.then(({ user, token }) => {
|
||||
this.setState({ success: true, loading: false, error: null });
|
||||
store.dispatch(handleSuccessfulLogin(user, token));
|
||||
})
|
||||
.catch(error => {
|
||||
if (!error.status || error.status !== 401) {
|
||||
console.error(error);
|
||||
}
|
||||
const changeSet = { success: false, loading: false, error };
|
||||
if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
changeSet.requireRecaptcha = !!this.context.store.getState().config
|
||||
.static.TALK_RECAPTCHA_PUBLIC;
|
||||
} else if (error.translation_key === 'EMAIL_NOT_VERIFIED') {
|
||||
changeSet.requireEmailConfirmation = true;
|
||||
}
|
||||
this.setState(changeSet);
|
||||
});
|
||||
};
|
||||
|
||||
getErrorMessage() {
|
||||
if (!this.state.error) {
|
||||
return null;
|
||||
}
|
||||
return this.state.error.translation_key === 'NOT_AUTHORIZED'
|
||||
? t('error.email_password')
|
||||
: translateError(this.state.error);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
signIn={this.signIn}
|
||||
loading={this.state.loading}
|
||||
errorMessage={this.getErrorMessage()}
|
||||
requireRecaptcha={this.state.requireRecaptcha}
|
||||
requireEmailConfirmation={this.state.requireEmailConfirmation}
|
||||
success={this.state.success}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithSignIn;
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translateError } from '../utils';
|
||||
import validate from '../helpers/validate';
|
||||
import errorMsg from 'coral-framework/helpers/error';
|
||||
import t from '../services/i18n';
|
||||
import withQuery from './withQuery';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const requiredFields = ['username', 'email', 'password'];
|
||||
const allFields = requiredFields;
|
||||
|
||||
const QUERY = gql`
|
||||
query TalkFramework_WithSignUpQuery {
|
||||
settings {
|
||||
requireEmailConfirmation
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const withSettingsQuery = withQuery(QUERY);
|
||||
|
||||
/**
|
||||
* withSignUp provides properties
|
||||
* `signUp`,
|
||||
* `loading`,
|
||||
* `errorMessage`,
|
||||
* `requireEmailVerification`,
|
||||
* `success`,
|
||||
* `validate`.
|
||||
*/
|
||||
const withSignUp = hoistStatics(WrappedComponent => {
|
||||
class WithSignUp extends React.Component {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
rest: PropTypes.func,
|
||||
pym: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
root: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
success: false,
|
||||
};
|
||||
|
||||
validate = (field, value) => {
|
||||
if (!allFields.includes(field)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (requiredFields.includes(field) && !value) {
|
||||
return t('error.required_field');
|
||||
}
|
||||
|
||||
if (field in validate) {
|
||||
return validate[field](value) ? null : errorMsg[field];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
signUp = ({ username, email, password }, redirectUri) => {
|
||||
if (!redirectUri) {
|
||||
redirectUri = this.context.pym.parentUrl || location.href;
|
||||
}
|
||||
|
||||
const { rest } = this.context;
|
||||
const params = {
|
||||
method: 'POST',
|
||||
body: {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
},
|
||||
headers: { 'X-Pym-Url': redirectUri },
|
||||
};
|
||||
|
||||
rest('/users', params)
|
||||
.then(() => {
|
||||
this.setState({ success: true, loading: false, error: null });
|
||||
})
|
||||
.catch(error => {
|
||||
if (!error.status || error.status !== 401) {
|
||||
console.error(error);
|
||||
}
|
||||
const changeSet = { success: false, loading: false, error };
|
||||
this.setState(changeSet);
|
||||
});
|
||||
};
|
||||
|
||||
getErrorMessage() {
|
||||
if (!this.state.error) {
|
||||
return null;
|
||||
}
|
||||
return translateError(this.state.error);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
signUp={this.signUp}
|
||||
loading={this.state.loading}
|
||||
errorMessage={this.getErrorMessage()}
|
||||
requireEmailConfirmation={
|
||||
!!get(this.props, 'root.settings.requireEmailConfirmation')
|
||||
}
|
||||
success={this.state.success}
|
||||
validate={this.validate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithSignUp;
|
||||
});
|
||||
|
||||
export default compose(withSettingsQuery, withSignUp);
|
||||
@@ -0,0 +1,61 @@
|
||||
import * as actions from '../constants/auth';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
const initialState = {
|
||||
checkedInitialLogin: false,
|
||||
initialLoginError: null,
|
||||
user: null,
|
||||
};
|
||||
|
||||
const purge = user => {
|
||||
const {settings, ...userData} = user; // eslint-disable-line
|
||||
return userData;
|
||||
};
|
||||
|
||||
export default function auth(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
initialLoginError: action.error,
|
||||
checkedInitialLogin: true,
|
||||
user: null,
|
||||
};
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
checkedInitialLogin: true,
|
||||
user: action.user ? purge(action.user) : null,
|
||||
};
|
||||
case actions.HANDLE_SUCCESSFUL_LOGIN:
|
||||
return {
|
||||
...state,
|
||||
user: action.user ? purge(action.user) : null,
|
||||
};
|
||||
case actions.LOGOUT:
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
};
|
||||
case actions.UPDATE_STATUS: {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
status: merge({}, state.user.status, action.status),
|
||||
},
|
||||
};
|
||||
}
|
||||
case actions.UPDATE_USERNAME:
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
username: action.username,
|
||||
lowercaseUsername: action.username.toLowerCase(),
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
+4
-6
@@ -1,15 +1,13 @@
|
||||
import * as actions from '../actions/config';
|
||||
import { MERGE_CONFIG } from '../constants/config';
|
||||
|
||||
const initialState = {
|
||||
data: {},
|
||||
};
|
||||
const initialState = {};
|
||||
|
||||
export default function config(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CONFIG_UPDATED:
|
||||
case MERGE_CONFIG:
|
||||
return {
|
||||
...state,
|
||||
data: action.data,
|
||||
...action.config,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
@@ -0,0 +1,8 @@
|
||||
import auth from './auth';
|
||||
import config from './config';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
login: auth,
|
||||
config,
|
||||
};
|
||||
+55
-4
@@ -22,6 +22,10 @@ import {
|
||||
import { createHistory } from 'coral-framework/services/history';
|
||||
import { createIntrospection } from 'coral-framework/services/introspection';
|
||||
import introspectionData from 'coral-framework/graphql/introspection.json';
|
||||
import coreReducers from '../reducers';
|
||||
import { checkLogin as checkLoginAction } from '../actions/auth';
|
||||
import { mergeConfig } from '../actions/config';
|
||||
import { setAuthToken, logout } from '../actions/auth';
|
||||
|
||||
/**
|
||||
* getAuthToken returns the active auth token or null
|
||||
@@ -32,7 +36,7 @@ import introspectionData from 'coral-framework/graphql/introspection.json';
|
||||
const getAuthToken = (store, storage) => {
|
||||
let state = store.getState();
|
||||
|
||||
if (state.config.auth_token) {
|
||||
if (state.config && state.config.auth_token) {
|
||||
// if an auth_token exists in config, use it.
|
||||
return state.config.auth_token;
|
||||
} else if (!bowser.safari && !bowser.ios && storage) {
|
||||
@@ -51,6 +55,19 @@ function areWeInIframe() {
|
||||
}
|
||||
}
|
||||
|
||||
function initExternalConfig({ store, pym, inIframe }) {
|
||||
if (!inIframe) {
|
||||
return;
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
pym.sendMessage('getConfig');
|
||||
pym.onMessage('config', config => {
|
||||
store.dispatch(mergeConfig(JSON.parse(config)));
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* createContext setups and returns Talk dependencies that should be
|
||||
* passed to `TalkProvider`.
|
||||
@@ -70,6 +87,8 @@ export async function createContext({
|
||||
notification,
|
||||
preInit,
|
||||
init = noop,
|
||||
checkLogin = true,
|
||||
addExternalConfig = true,
|
||||
} = {}) {
|
||||
const inIframe = areWeInIframe();
|
||||
const eventEmitter = new EventEmitter({ wildcard: true });
|
||||
@@ -98,7 +117,8 @@ export async function createContext({
|
||||
token,
|
||||
});
|
||||
|
||||
let { LIVE_URI: liveUri } = getStaticConfiguration();
|
||||
const staticConfig = getStaticConfiguration();
|
||||
let { LIVE_URI: liveUri } = staticConfig;
|
||||
if (liveUri == null) {
|
||||
// The protocol must match the origin protocol, secure/insecure.
|
||||
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
@@ -161,6 +181,7 @@ export async function createContext({
|
||||
|
||||
// Create our redux store.
|
||||
const finalReducers = {
|
||||
...coreReducers,
|
||||
...reducers,
|
||||
...plugins.getReducers(),
|
||||
};
|
||||
@@ -180,9 +201,39 @@ export async function createContext({
|
||||
[client.middleware(), apolloErrorReporter, createReduxEmitter(eventEmitter)]
|
||||
);
|
||||
|
||||
// Run pre initialization.
|
||||
if (inIframe) {
|
||||
pym.onMessage('login', token => {
|
||||
if (token) {
|
||||
store.dispatch(setAuthToken(token));
|
||||
}
|
||||
});
|
||||
|
||||
pym.onMessage('logout', () => {
|
||||
store.dispatch(logout());
|
||||
});
|
||||
}
|
||||
|
||||
const preInitList = [];
|
||||
|
||||
store.dispatch(
|
||||
mergeConfig({
|
||||
static: staticConfig,
|
||||
})
|
||||
);
|
||||
|
||||
if (preInit) {
|
||||
await preInit(context);
|
||||
preInitList.push(preInit(context));
|
||||
}
|
||||
|
||||
if (addExternalConfig) {
|
||||
preInitList.push(initExternalConfig(context));
|
||||
}
|
||||
|
||||
// Run pre initialization.
|
||||
await Promise.all(preInitList);
|
||||
|
||||
if (checkLogin) {
|
||||
store.dispatch(checkLoginAction());
|
||||
}
|
||||
|
||||
// Run initialization.
|
||||
|
||||
@@ -6,6 +6,7 @@ import flattenDeep from 'lodash/flattenDeep';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import flatten from 'lodash/flatten';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import get from 'lodash/get';
|
||||
import { getDisplayName } from 'coral-framework/helpers/hoc';
|
||||
import camelize from '../helpers/camelize';
|
||||
|
||||
@@ -83,7 +84,7 @@ class PluginsService {
|
||||
* query datas are only passed to the component if it is defined in `component.fragments`.
|
||||
*/
|
||||
getSlotComponentProps(component, reduxState, props, queryData) {
|
||||
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
|
||||
const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig;
|
||||
return {
|
||||
...props,
|
||||
config: pluginConfig,
|
||||
@@ -97,7 +98,7 @@ class PluginsService {
|
||||
* Returns React Elements for given slot.
|
||||
*/
|
||||
getSlotElements(slot, reduxState, props = {}, queryData = {}) {
|
||||
const pluginConfig = reduxState.config.plugin_config || emptyConfig;
|
||||
const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig;
|
||||
|
||||
const isDisabled = component => {
|
||||
if (
|
||||
|
||||
@@ -252,3 +252,12 @@ export function mapLeaves(o, mapper) {
|
||||
return mapper(val);
|
||||
});
|
||||
}
|
||||
|
||||
export function translateError(error) {
|
||||
if (error.translation_key) {
|
||||
return t(`error.${error.translation_key}`);
|
||||
} else if (error.networkError) {
|
||||
return t('error.network_error');
|
||||
}
|
||||
return error.toString();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
class Main extends React.Component {
|
||||
render() {
|
||||
return <Slot fill="login" />;
|
||||
}
|
||||
}
|
||||
|
||||
export default Main;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
|
||||
import { createContext } from 'coral-framework/services/bootstrap';
|
||||
import Main from './containers/Main';
|
||||
import TalkProvider from 'coral-framework/components/TalkProvider';
|
||||
import pluginsConfig from 'pluginsConfig';
|
||||
|
||||
async function main() {
|
||||
const context = await createContext({
|
||||
pluginsConfig,
|
||||
});
|
||||
render(
|
||||
<TalkProvider {...context}>
|
||||
<Main />
|
||||
</TalkProvider>,
|
||||
document.querySelector('#talk-login-container')
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,3 @@
|
||||
.bare {
|
||||
composes: buttonReset from "coral-framework/styles/reset.css";
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './BareButton.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
/**
|
||||
* BareButton is a button whose styling is stripped off to a minimum.
|
||||
* Can pass anchor=true to use `a` instead of `button`
|
||||
*/
|
||||
const BareButton = ({ anchor, className, ...props }) => {
|
||||
let Element = 'button';
|
||||
if (anchor) {
|
||||
Element = 'a';
|
||||
}
|
||||
return <Element {...props} className={cn(styles.bare, className)} />;
|
||||
};
|
||||
|
||||
BareButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
anchor: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default BareButton;
|
||||
@@ -10,6 +10,7 @@
|
||||
line-height: 22px;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -28,3 +28,4 @@ export { default as Label } from './components/Label';
|
||||
export { default as FlagLabel } from './components/FlagLabel';
|
||||
export { default as Dropdown } from './components/Dropdown';
|
||||
export { default as Option } from './components/Option';
|
||||
export { default as BareButton } from './components/BareButton';
|
||||
|
||||
@@ -44,6 +44,10 @@ const CONFIG = {
|
||||
// fetching again.
|
||||
SETTINGS_CACHE_TIME: ms(process.env.TALK_SETTINGS_CACHE_TIME || '1hr'),
|
||||
|
||||
// ALLOW_NO_LIMIT_QUERIES enables some queries to specify a limit of -1 to
|
||||
// request all of the records. Otherwise, minimum limits of 0 are enforced.
|
||||
ALLOW_NO_LIMIT_QUERIES: process.env.TALK_ALLOW_NO_LIMIT_QUERIES === 'TRUE',
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// JWT based configuration
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -84,23 +84,3 @@ TALK_JWT_SECRET=jX9y8G2ApcVLwyL{$6s3
|
||||
Be default, we sign our tokens with HMAC using a SHA-256 hash algorithm. If you
|
||||
want to change the signing algorithm, or use multiple signing/verifying keys,
|
||||
refer to our [Advanced Configuration]({{ "/advanced-configuration/" | relative_url }}) documentation.
|
||||
|
||||
## TALK_FACEBOOK_APP_ID
|
||||
|
||||
The Facebook App ID for your Facebook Login enabled app. You can learn more
|
||||
about getting a Facebook App ID at the
|
||||
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
|
||||
or by visiting the
|
||||
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
|
||||
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||
enabled.
|
||||
|
||||
## TALK_FACEBOOK_APP_SECRET
|
||||
|
||||
The Facebook App Secret for your Facebook Login enabled app. You can learn more
|
||||
about getting a Facebook App Secret at the
|
||||
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
|
||||
or by visiting the
|
||||
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
|
||||
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||
enabled.
|
||||
|
||||
@@ -57,6 +57,26 @@ When `TRUE`, it will not mount the static asset serving routes on the router.
|
||||
This is used primarily in conjunction with [TALK_STATIC_URI](#talk_static_uri){: .param}
|
||||
when the static assets are being hosted on an external domain. (Default `FALSE`)
|
||||
|
||||
## TALK_FACEBOOK_APP_ID
|
||||
|
||||
The Facebook App ID for your Facebook Login enabled app. You can learn more
|
||||
about getting a Facebook App ID at the
|
||||
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
|
||||
or by visiting the
|
||||
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
|
||||
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||
enabled.
|
||||
|
||||
## TALK_FACEBOOK_APP_SECRET
|
||||
|
||||
The Facebook App Secret for your Facebook Login enabled app. You can learn more
|
||||
about getting a Facebook App Secret at the
|
||||
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
|
||||
or by visiting the
|
||||
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
|
||||
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||
enabled.
|
||||
|
||||
## TALK_HELMET_CONFIGURATION
|
||||
|
||||
A JSON string representing the configuration passed to the
|
||||
@@ -303,6 +323,8 @@ the websocket to keep the socket alive, parsed by
|
||||
|
||||
## TALK_RECAPTCHA_PUBLIC
|
||||
|
||||
Setting a reCAPTCHA Public and Secret key will enable and require reCAPTCHA upon multiple failed login attempts.
|
||||
|
||||
Client secret used for enabling reCAPTCHA powered logins. If
|
||||
[TALK_RECAPTCHA_SECRET](#talk_recaptcha_secret){: .param} and
|
||||
[TALK_RECAPTCHA_PUBLIC](#talk_recaptcha_public){: .param} are not provided it will instead
|
||||
@@ -486,4 +508,34 @@ Used to set the key for use with
|
||||
[Apollo Engine](https://www.apollographql.com/engine/){:target="_blank"} for
|
||||
tracing of GraphQL requests.
|
||||
|
||||
**Note: Apollo Engine is a premium service, charges may apply.**
|
||||
**Note: Apollo Engine is a premium service, charges may apply.**
|
||||
|
||||
## ALLOW_NO_LIMIT_QUERIES
|
||||
|
||||
Setting this to `TRUE` will allow queries to execute without a limit (returns
|
||||
all documents). This introduces a significant performance regression, and should
|
||||
be used with caution. (Default `FALSE`)
|
||||
|
||||
## TALK_ADDTL_COMMENTS_ON_LOAD_MORE
|
||||
|
||||
This is a **Build Variable** and must be consumed during build. If using the
|
||||
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
|
||||
image you can specify it with `--build-arg TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10`.
|
||||
|
||||
Specifies the number of additional comments to load when a user clicks `Load More`. (Default `10`)
|
||||
|
||||
## TALK_ASSET_COMMENTS_LOAD_DEPTH
|
||||
|
||||
This is a **Build Variable** and must be consumed during build. If using the
|
||||
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
|
||||
image you can specify it with `--build-arg TALK_ASSET_COMMENTS_LOAD_DEPTH=10`.
|
||||
|
||||
Specifies the initial number of comments to load for an asset. (Default `10`)
|
||||
|
||||
## TALK_REPLY_COMMENTS_LOAD_DEPTH
|
||||
|
||||
This is a **Build Variable** and must be consumed during build. If using the
|
||||
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
|
||||
image you can specify it with `--build-arg TALK_REPLY_COMMENTS_LOAD_DEPTH=3`.
|
||||
|
||||
Specifies the initial replies to load for a comment. (Default `3`)
|
||||
@@ -116,8 +116,8 @@ point with the still spam-like comment, the comment will be reported as containi
|
||||
|
||||
Configuration:
|
||||
|
||||
- `TALK_AKISMET_API_KEY` (**required**) - The Akismet API key located on your account page
|
||||
- `TALK_AKISMET_SITE` (**required**) - The URL for your site that the comment are appearing on (not the root url, the url for the articles)
|
||||
- `TALK_AKISMET_API_KEY` (**required**) - The Akismet API key located on your account page.
|
||||
- `TALK_AKISMET_SITE` (**required**) - The URL where you are embedding the comment stream on to provide context to Akismet. If you're hosting talk on https://talk.mynews.org/, and your news site is https://mynews.org/, then you should set this parameter to `https://mynews.org/`
|
||||
|
||||
## talk-plugin-notifications
|
||||
|
||||
@@ -148,4 +148,4 @@ anything. You need to enable one of the `talk-plugin-notifications-category-*` p
|
||||
Source: [plugins/talk-plugin-notifications-category-reply](https://github.com/coralproject/talk/tree/master/plugins/talk-plugin-notifications-category-reply){:target="_blank"}
|
||||
|
||||
Replies made to each user will trigger an email to be sent with the notification
|
||||
details if enabled.
|
||||
details if enabled.
|
||||
|
||||
+156
-28
@@ -1,54 +1,182 @@
|
||||
const DataLoader = require('dataloader');
|
||||
|
||||
const util = require('./util');
|
||||
|
||||
const ActionsService = require('../../services/actions');
|
||||
const ActionModel = require('../../models/action');
|
||||
const { first, get, merge, remove, groupBy, reduce } = require('lodash');
|
||||
|
||||
/**
|
||||
* Gets actions based on their item id's.
|
||||
*/
|
||||
const genActionsByItemID = (_, item_ids) => {
|
||||
return ActionsService.findByItemIdArray(item_ids).then(
|
||||
const genActionsByItemID = (
|
||||
{ connectors: { services: { Actions } } },
|
||||
item_ids
|
||||
) => {
|
||||
return Actions.findByItemIdArray(item_ids).then(
|
||||
util.arrayJoinBy(item_ids, 'item_id')
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Looks up actions based on the requested id's all bounded by the user.
|
||||
* @param {Object} context the context of the request
|
||||
* @param {Array} ids array of id's to get
|
||||
* @return {Promise} resolves to the promises of the requested actions
|
||||
* Looks up the actions for each of the items.
|
||||
*
|
||||
* @param {Object} ctx the graph context of the request
|
||||
* @param {Array<String>} itemIDs the items that we need to get the actions for
|
||||
*/
|
||||
const genActionSummariessByItemID = ({ user = {} }, item_ids) => {
|
||||
return ActionsService.getActionSummaries(item_ids, user.id).then(
|
||||
util.arrayJoinBy(item_ids, 'item_id')
|
||||
const genActionsAuthoredWithID = (
|
||||
{ user = {}, connectors: { services: { Actions } } },
|
||||
itemIDs
|
||||
) =>
|
||||
Actions.getUserActions(user.id, itemIDs).then(
|
||||
util.arrayJoinBy(itemIDs, 'item_id')
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Search for actions based on their action_type and item_type and ensures that
|
||||
* the actions returned have unique item id's.
|
||||
* @param {String} action_type the action to search by
|
||||
* @param {String} item_type the item id to search by
|
||||
* @return {Promise} resolves to distinct items actions
|
||||
* iterateActionCounts will create an iterable object that can be used to
|
||||
* compute action summaries.
|
||||
*
|
||||
* @param {Object} action_counts the action count object
|
||||
*/
|
||||
const getItemIdsByActionTypeAndItemType = (_, action_type, item_type) => {
|
||||
return ActionModel.distinct('item_id', { action_type, item_type });
|
||||
const iterateActionCounts = action_counts =>
|
||||
Object.keys(action_counts).map(action_type => ({
|
||||
count: action_counts[action_type],
|
||||
action_type: action_type.toUpperCase(),
|
||||
}));
|
||||
|
||||
/**
|
||||
* getUserActions will get the actions made by the user for this specific
|
||||
* item.
|
||||
*
|
||||
* @param {Object} ctx the graph context of the request
|
||||
* @param {Object} item the item that we're getting the actions for
|
||||
*/
|
||||
async function getUserActions(ctx, { action_counts, id }) {
|
||||
const { loaders: { Actions } } = ctx;
|
||||
|
||||
// Get the total count for all action types.
|
||||
const totalActionCount = reduce(
|
||||
action_counts,
|
||||
(total, count) => total + count,
|
||||
0
|
||||
);
|
||||
|
||||
// Check to see if there are any user actions to get.
|
||||
const hasUserActions = ctx.user && totalActionCount > 0;
|
||||
if (!hasUserActions) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Possibly get the list of user actions completed by the user. This will be
|
||||
// used later to join together with the action summaries to provide context.
|
||||
const userActions = await Actions.getAuthoredByID.load(id);
|
||||
if (userActions.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Group the user actions in the same way that the action counts are
|
||||
// grouped. This will let us extract it easy.
|
||||
return reduce(
|
||||
groupBy(userActions, ({ action_type, group_id }) =>
|
||||
(group_id ? `${action_type}_${group_id}` : action_type).toUpperCase()
|
||||
),
|
||||
(allUserActions, userActions, actionType) =>
|
||||
merge(allUserActions, { [actionType]: first(userActions) }),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
// This will match any action count that is specific for a group id.
|
||||
const nonGroupIDTest = /^([A-Z]+)_([A-Z_]+)$/;
|
||||
|
||||
/**
|
||||
* resolveActionSummariesForItem will resolve the action summaries for an item.
|
||||
*
|
||||
* @param {Object} ctx the graph context of the request
|
||||
* @param {Object} item the item that we are resolving an action summary for
|
||||
*/
|
||||
async function resolveActionSummariesForItem(ctx, { id, action_counts }) {
|
||||
// Cache all those entries for which we got the group id of, because we
|
||||
// don't want to include them twice.
|
||||
const groupIDCache = {};
|
||||
|
||||
// Get the user actions for this specific item.
|
||||
const groupedUserActions = await getUserActions(ctx, { id, action_counts });
|
||||
|
||||
// Generate the action summaries for the item.
|
||||
return iterateActionCounts(action_counts).reduce(
|
||||
(actionTypeList, { count, action_type }) => {
|
||||
// Get the current user's actions (if they have any).
|
||||
const current_user = get(groupedUserActions, action_type, null);
|
||||
|
||||
// Check to see if this is a action without a corresponding group id.
|
||||
if (nonGroupIDTest.test(action_type)) {
|
||||
// This action type does have a group id associated with it.
|
||||
const results = nonGroupIDTest.exec(action_type);
|
||||
const groupActionType = results[1];
|
||||
const groupID = results[2];
|
||||
|
||||
// Purge out the summary if it already exists, and mark that this
|
||||
// group id has been found so we don't include it in the future.
|
||||
remove(
|
||||
actionTypeList,
|
||||
({ action_type }) => action_type === groupActionType
|
||||
);
|
||||
groupIDCache[groupActionType] = true;
|
||||
|
||||
// Push the new entry in.
|
||||
actionTypeList.push({
|
||||
action_type: groupActionType,
|
||||
group_id: groupID,
|
||||
count,
|
||||
current_user,
|
||||
});
|
||||
} else {
|
||||
// This does not have a group id. Check to see if this group id
|
||||
// already has an specific (group id) entry.
|
||||
if (groupIDCache[action_type]) {
|
||||
// It does. Don't add anything.
|
||||
return actionTypeList;
|
||||
}
|
||||
|
||||
// It does not, add the entry.
|
||||
actionTypeList.push({
|
||||
action_type,
|
||||
group_id: null,
|
||||
count,
|
||||
current_user,
|
||||
});
|
||||
}
|
||||
|
||||
return actionTypeList;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the action summaries for a set of items.
|
||||
*
|
||||
* @param {Object} ctx the graph context of the request
|
||||
* @param {Array<Object>} items the items that should have their items looked up for
|
||||
*/
|
||||
const genActionSummariesByItem = async (ctx, items) => {
|
||||
// This is designed to match the action_counts value that is embedded on
|
||||
// documents which cache action counts. For users that are not logged in, we
|
||||
// don't need to hit the actions collection at all!
|
||||
|
||||
// We will literate over all the items that we're comparing.
|
||||
return items.map(item => resolveActionSummariesForItem(ctx, item));
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
* @param {Object} ctx the context of the GraphQL request
|
||||
* @return {Object} object of loaders
|
||||
*/
|
||||
module.exports = context => ({
|
||||
module.exports = ctx => ({
|
||||
Actions: {
|
||||
getByID: new DataLoader(ids => genActionsByItemID(context, ids)),
|
||||
getSummariesByItemID: new DataLoader(ids =>
|
||||
genActionSummariessByItemID(context, ids)
|
||||
getByID: new DataLoader(ids => genActionsByItemID(ctx, ids)),
|
||||
getSummariesByItem: new DataLoader(
|
||||
items => genActionSummariesByItem(ctx, items),
|
||||
{ cacheKeyFn: ({ id }) => id }
|
||||
),
|
||||
getByTypes: ({ action_type, item_type }) =>
|
||||
getItemIdsByActionTypeAndItemType(context, action_type, item_type),
|
||||
getAuthoredByID: new DataLoader(ids => genActionsAuthoredWithID(ctx, ids)),
|
||||
},
|
||||
});
|
||||
|
||||
+44
-43
@@ -4,7 +4,10 @@ const {
|
||||
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS,
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
} = require('../../perms/constants');
|
||||
const { CACHE_EXPIRY_COMMENT_COUNT } = require('../../config');
|
||||
const {
|
||||
CACHE_EXPIRY_COMMENT_COUNT,
|
||||
ALLOW_NO_LIMIT_QUERIES,
|
||||
} = require('../../config');
|
||||
const ms = require('ms');
|
||||
const sc = require('snake-case');
|
||||
|
||||
@@ -148,12 +151,11 @@ const getCommentCountByQuery = (ctx, options) => {
|
||||
* @param {Object} params the params from the client describing the query
|
||||
*/
|
||||
const getStartCursor = (ctx, nodes, { cursor, sortBy }) => {
|
||||
switch (sortBy) {
|
||||
case 'CREATED_AT':
|
||||
return nodes.length ? nodes[0].created_at : null;
|
||||
case 'REPLIES':
|
||||
// The cursor is the start! This is using numeric pagination.
|
||||
return cursor != null ? cursor : 0;
|
||||
if (sortBy === 'CREATED_AT') {
|
||||
return nodes.length ? nodes[0].created_at : null;
|
||||
} else if (sortBy === 'REPLIES') {
|
||||
// The cursor is the start! This is using numeric pagination.
|
||||
return cursor != null ? cursor : 0;
|
||||
}
|
||||
|
||||
const SORT_KEY = sortBy.toLowerCase();
|
||||
@@ -181,11 +183,10 @@ const getStartCursor = (ctx, nodes, { cursor, sortBy }) => {
|
||||
* @param {Object} params the params from the client describing the query
|
||||
*/
|
||||
const getEndCursor = (ctx, nodes, { cursor, sortBy }) => {
|
||||
switch (sortBy) {
|
||||
case 'CREATED_AT':
|
||||
return nodes.length ? nodes[nodes.length - 1].created_at : null;
|
||||
case 'REPLIES':
|
||||
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
|
||||
if (sortBy === 'CREATED_AT') {
|
||||
return nodes.length ? nodes[nodes.length - 1].created_at : null;
|
||||
} else if (sortBy === 'REPLIES') {
|
||||
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
|
||||
}
|
||||
|
||||
const SORT_KEY = sortBy.toLowerCase();
|
||||
@@ -212,36 +213,33 @@ const getEndCursor = (ctx, nodes, { cursor, sortBy }) => {
|
||||
* @param {Object} params the params from the client describing the query
|
||||
*/
|
||||
const applySort = (ctx, query, { cursor, sortOrder, sortBy }) => {
|
||||
switch (sortBy) {
|
||||
case 'CREATED_AT': {
|
||||
if (cursor) {
|
||||
if (sortOrder === 'DESC') {
|
||||
query = query.where({
|
||||
created_at: {
|
||||
$lt: cursor,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
query = query.where({
|
||||
created_at: {
|
||||
$gt: cursor,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (sortBy === 'CREATED_AT') {
|
||||
if (cursor) {
|
||||
if (sortOrder === 'DESC') {
|
||||
query = query.where({
|
||||
created_at: {
|
||||
$lt: cursor,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
query = query.where({
|
||||
created_at: {
|
||||
$gt: cursor,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return query.sort({ created_at: sortOrder === 'DESC' ? -1 : 1 });
|
||||
}
|
||||
case 'REPLIES': {
|
||||
if (cursor) {
|
||||
query = query.skip(cursor);
|
||||
}
|
||||
|
||||
return query.sort({
|
||||
reply_count: sortOrder === 'DESC' ? -1 : 1,
|
||||
created_at: sortOrder === 'DESC' ? -1 : 1,
|
||||
});
|
||||
return query.sort({ created_at: sortOrder === 'DESC' ? -1 : 1 });
|
||||
} else if (sortBy === 'REPLIES') {
|
||||
if (cursor) {
|
||||
query = query.skip(cursor);
|
||||
}
|
||||
|
||||
return query.sort({
|
||||
reply_count: sortOrder === 'DESC' ? -1 : 1,
|
||||
created_at: sortOrder === 'DESC' ? -1 : 1,
|
||||
});
|
||||
}
|
||||
|
||||
const SORT_KEY = sortBy.toLowerCase();
|
||||
@@ -280,7 +278,7 @@ const executeWithSort = async (
|
||||
query = applySort(ctx, query, { cursor, sortOrder, sortBy });
|
||||
|
||||
// Apply the limit (if it exists, as it's applied universally).
|
||||
if (limit) {
|
||||
if (limit >= 0) {
|
||||
query = query.limit(limit + 1);
|
||||
}
|
||||
|
||||
@@ -290,7 +288,7 @@ const executeWithSort = async (
|
||||
// The hasNextPage is always handled the same (ask for one more than we need,
|
||||
// if there is one more, than there is more).
|
||||
let hasNextPage = false;
|
||||
if (limit && nodes.length > limit) {
|
||||
if (limit >= 0 && nodes.length > limit) {
|
||||
// There was one more than we expected! Set hasNextPage = true and remove
|
||||
// the last item from the array that we requested.
|
||||
hasNextPage = true;
|
||||
@@ -302,11 +300,9 @@ const executeWithSort = async (
|
||||
return {
|
||||
startCursor: getStartCursor(ctx, nodes, {
|
||||
cursor,
|
||||
sortOrder,
|
||||
sortBy,
|
||||
limit,
|
||||
}),
|
||||
endCursor: getEndCursor(ctx, nodes, { cursor, sortOrder, sortBy, limit }),
|
||||
endCursor: getEndCursor(ctx, nodes, { cursor, sortBy }),
|
||||
hasNextPage,
|
||||
nodes,
|
||||
};
|
||||
@@ -338,6 +334,11 @@ const getCommentsByQuery = async (
|
||||
) => {
|
||||
let comments = CommentModel.find();
|
||||
|
||||
// Enforce that the limit must be gte 0 if this option is not true.
|
||||
if (!ALLOW_NO_LIMIT_QUERIES && limit < 0) {
|
||||
throw new Error('cannot query for limit < 0');
|
||||
}
|
||||
|
||||
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
|
||||
// special privileges.
|
||||
if (
|
||||
|
||||
@@ -35,12 +35,12 @@ const Comment = {
|
||||
actions({ id }, _, { loaders: { Actions } }) {
|
||||
return Actions.getByID.load(id);
|
||||
},
|
||||
action_summaries({ id, action_summaries }, _, { loaders: { Actions } }) {
|
||||
if (action_summaries) {
|
||||
return action_summaries;
|
||||
action_summaries(comment, _, { loaders: { Actions } }) {
|
||||
if (comment.action_summaries) {
|
||||
return comment.action_summaries;
|
||||
}
|
||||
|
||||
return Actions.getSummariesByItemID.load(id);
|
||||
return Actions.getSummariesByItem.load(comment);
|
||||
},
|
||||
asset({ asset_id }, _, { loaders: { Assets } }) {
|
||||
return Assets.getByID.load(asset_id);
|
||||
|
||||
@@ -16,8 +16,8 @@ const {
|
||||
const { property } = require('lodash');
|
||||
|
||||
const User = {
|
||||
action_summaries({ id }, _, { loaders: { Actions } }) {
|
||||
return Actions.getSummariesByItemID.load(id);
|
||||
action_summaries(user, _, { loaders: { Actions } }) {
|
||||
return Actions.getSummariesByItem.load(user);
|
||||
},
|
||||
actions({ id }, _, { loaders: { Actions } }) {
|
||||
return Actions.getByID.load(id);
|
||||
|
||||
@@ -23,6 +23,7 @@ en:
|
||||
click_to_confirm: "Click below to confirm your email address"
|
||||
confirm: "Confirm"
|
||||
password_reset:
|
||||
mail_sent: 'If you have a registered account, a password reset link was sent to that email'
|
||||
set_new_password: "Change Your Password"
|
||||
new_password: "New Password"
|
||||
new_password_help: "Password must be at least 8 characters"
|
||||
@@ -236,6 +237,7 @@ en:
|
||||
password: "Password must be at least 8 characters"
|
||||
username: "Usernames can contain letters numbers and _ only"
|
||||
unexpected: "Unexpected error occurred. Sorry!"
|
||||
required_field: "This field is required"
|
||||
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
|
||||
flag_comment: "Report comment"
|
||||
flag_reason: "Reason for reporting (Optional)"
|
||||
|
||||
+2
-1
@@ -27,7 +27,7 @@ es:
|
||||
new_password: "New Password"
|
||||
new_password_help: "Password must be at least 8 characters"
|
||||
confirm_new_password: "Confirm New Password"
|
||||
change_password: "Change Password"
|
||||
change_password: "Change Password"
|
||||
characters_remaining: "carácteres restantes"
|
||||
comment:
|
||||
anon: Anónimo
|
||||
@@ -235,6 +235,7 @@ es:
|
||||
organization_name: "El nombre de la organización debe contener letras y/o números."
|
||||
password: "La contraseña debe tener por lo menos 8 caracteres"
|
||||
username: "Los nombres pueden contener letras números y _"
|
||||
required_field: "Este campo es requerido"
|
||||
unexpected: "Lo siento. Ha habido un error no previsto."
|
||||
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
|
||||
flag_comment: "Reportar este comentario"
|
||||
|
||||
+2
-1
@@ -2,7 +2,7 @@ fr:
|
||||
your_account_has_been_suspended: Your account has been temporarily suspended.
|
||||
your_account_has_been_banned: Your account has been banned.
|
||||
your_username_has_been_rejected: Your account has been suspended because your username has been deemed inappropriate. To restore your account please enter a new username.
|
||||
embed_comments_tab: Comments
|
||||
embed_comments_tab: Comments
|
||||
bandialog:
|
||||
are_you_sure: "Êtes-vous sûr de vouloir bannir {0}?"
|
||||
ban_user: "Bannir l'utilisateur ?"
|
||||
@@ -235,6 +235,7 @@ fr:
|
||||
organization_name: "Le nom de l'organisation ne peut contenir que des lettres ou des chiffres."
|
||||
password: "Le mot de passe doit être d'au moins 8 caractères"
|
||||
username: "Les noms d'utilisateur ne peuvent contenir que des chiffres, des lettres et \"_\""
|
||||
required_field: "Ce champ est obligatoire"
|
||||
unexpected: "Unexpected error occurred. Sorry!"
|
||||
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
|
||||
flag_comment: "Signaler un commentaire"
|
||||
|
||||
+19
-19
@@ -62,10 +62,10 @@ nl_NL:
|
||||
reactions: 'reacties'
|
||||
story: 'Verhaal'
|
||||
flagged_usernames:
|
||||
notify_approved: '{0} approved username {1}'
|
||||
notify_rejected: '{0} rejected username {1}'
|
||||
notify_flagged: '{0} reported username {1}'
|
||||
notify_changed: 'user {0} changed their username to {1}'
|
||||
notify_approved: '{0} heeft gebruikersnaam {1} goedgekeurd'
|
||||
notify_rejected: '{0} heeft gebruikersnaam {1} afgekeurd'
|
||||
notify_flagged: '{0} heeft gebruikersnaam {1} gerapporteerd'
|
||||
notify_changed: 'gebruiker {0} heeft zijn/haar gebruikersnaam veranderd naar {1}'
|
||||
community:
|
||||
account_creation_date: "Aanmaakdatum account"
|
||||
active: Actief
|
||||
@@ -256,7 +256,7 @@ nl_NL:
|
||||
label: "Nieuwe gebruikersnaam"
|
||||
msg: "Je account is momenteel geschorst omdat je gebruikersnaam ongepast is bevonden. Om je account te herstellen, moet je je gebruikersnaam herstellen. Neem contact met ons op wanneer je vragen hebt."
|
||||
changed_name:
|
||||
msg: "Your username change is under review by our moderation team."
|
||||
msg: "De verandering van je gebruikersnaam wordt door ons moderatieteam gecontroleerd."
|
||||
my_comments: "Mijn reacties"
|
||||
my_profile: "Mijn profiel"
|
||||
new_count: "Bekijk {0} meer {1}"
|
||||
@@ -344,7 +344,7 @@ nl_NL:
|
||||
no_agree_comment: "Ik ben het niet eens met deze reactie"
|
||||
no_like_bio: "Ik vind deze biografie niet leuk"
|
||||
no_like_username: "Ik vind deze gebruikersnaam niet leuk"
|
||||
already_flagged_username: "You have already flagged this username."
|
||||
already_flagged_username: "Je hebt deze gebruikersnaam al gemeld."
|
||||
other: Ander
|
||||
permalink: Deel
|
||||
personal_info: "Deze reactie onthult persoonlijk identificeerbare gegevens."
|
||||
@@ -420,18 +420,18 @@ nl_NL:
|
||||
user_bio: "Gebruikers biografie"
|
||||
username_flags: "Meldingen voor deze gebruikersnaam"
|
||||
user_detail:
|
||||
remove_suspension: "Remove Suspension"
|
||||
suspend: "Suspend User"
|
||||
remove_ban: "Remove Ban"
|
||||
ban: "Ban User"
|
||||
member_since: "Member Since"
|
||||
remove_suspension: "Schorsing opheffen"
|
||||
suspend: "Gebruiker schorsen"
|
||||
remove_ban: "Verbanning verwijderen"
|
||||
ban: "Gebruiker verbannen"
|
||||
member_since: "Lid sinds"
|
||||
email: "Email"
|
||||
total_comments: "Total Comments"
|
||||
reject_rate: "Reject Rate"
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
account_history: "Account History"
|
||||
total_comments: "Totaal aantal reacties"
|
||||
reject_rate: "Beoordeling verwijderen"
|
||||
reports: "Rapportages"
|
||||
all: "Alle"
|
||||
rejected: "Afgewezen"
|
||||
account_history: "Accountgeschiedenis"
|
||||
user_impersonating: "Deze gebruiker imiteert"
|
||||
user_no_comment: "Je hebt nog niet eerder gereageerd. Laat je mening horen!"
|
||||
username_offensive: "Dit is een aanstootgevende gebruikersnaam."
|
||||
@@ -459,5 +459,5 @@ nl_NL:
|
||||
launch: "Start Talk"
|
||||
close: "Installatie afsluiten"
|
||||
admin_sidebar:
|
||||
view_options: "View Options"
|
||||
sort_comments: "Sort Comments"
|
||||
view_options: "Bekijk opties"
|
||||
sort_comments: "Sorteer reacties"
|
||||
|
||||
@@ -236,6 +236,7 @@ zh_CN:
|
||||
password: "密码长度须至少为 8 字符"
|
||||
username: "用户名只能包含字母、数字跟下划线"
|
||||
unexpected: "发生了异常错误。对不起!"
|
||||
required_field: "该字段必填"
|
||||
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
|
||||
flag_comment: "举报评论"
|
||||
flag_reason: "举报理由(可选)"
|
||||
|
||||
@@ -236,6 +236,7 @@ zh_TW:
|
||||
password: "密碼必須至少8個字符"
|
||||
username: "用戶名只能包含字母、數字和下劃線。"
|
||||
unexpected: "發生了意外錯誤。抱歉!"
|
||||
required_field: "該字段必填"
|
||||
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
|
||||
flag_comment: "舉報評論"
|
||||
flag_reason: "舉報原因(可選)"
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "4.1.0",
|
||||
"version": "4.2.0",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"main": "app.js",
|
||||
"private": true,
|
||||
@@ -120,8 +120,8 @@
|
||||
"inquirer": "^3.2.2",
|
||||
"inquirer-autocomplete-prompt": "^0.12.1",
|
||||
"ioredis": "3.1.4",
|
||||
"joi": "^10.6.0",
|
||||
"jsonwebtoken": "^7.4.3",
|
||||
"joi": "^13.0.0",
|
||||
"jsonwebtoken": "^8.0.0",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"keymaster": "^1.6.2",
|
||||
"kue": "0.11.6",
|
||||
@@ -150,7 +150,7 @@
|
||||
"postcss-smart-import": "^0.5.1",
|
||||
"precss": "1.4.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"pym.js": "^1.1.1",
|
||||
"pym.js": "^1.3.2",
|
||||
"query-string": "^5.0.0",
|
||||
"react": "^15.4.2",
|
||||
"react-apollo": "^1.4.12",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
setAuthToken,
|
||||
handleSuccessfulLogin,
|
||||
logout,
|
||||
} from 'coral-framework/actions/auth';
|
||||
@@ -1 +1,2 @@
|
||||
export { setSort } from 'coral-embed-stream/src/actions/stream';
|
||||
export { showSignInDialog } from 'coral-embed-stream/src/actions/login';
|
||||
|
||||
@@ -26,3 +26,4 @@ export {
|
||||
export {
|
||||
default as StreamConfiguration,
|
||||
} from 'coral-framework/components/StreamConfiguration';
|
||||
export { default as Recaptcha } from 'coral-framework/components/Recaptcha';
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
export { default as withReaction } from './withReaction';
|
||||
export { default as withTags } from './withTags';
|
||||
export { default as withSortOption } from './withSortOption';
|
||||
export { default as withFragments } from 'coral-framework/hocs/withFragments';
|
||||
export { default as excludeIf } from 'coral-framework/hocs/excludeIf';
|
||||
export { default as connect } from 'coral-framework/hocs/connect';
|
||||
export { default as withEmit } from 'coral-framework/hocs/withEmit';
|
||||
export {
|
||||
connect,
|
||||
withEmit,
|
||||
excludeIf,
|
||||
withFragments,
|
||||
withForgotPassword,
|
||||
withSignIn,
|
||||
withSignUp,
|
||||
withResendEmailConfirmation,
|
||||
withSetUsername,
|
||||
} from 'coral-framework/hocs';
|
||||
export {
|
||||
withIgnoreUser,
|
||||
withBanUser,
|
||||
|
||||
@@ -15,8 +15,7 @@ import * as PropTypes from 'prop-types';
|
||||
import { getDefinitionName } from '../utils';
|
||||
import { t, can } from 'plugin-api/beta/client/services';
|
||||
|
||||
// TODO: Auth logic needs refactoring.
|
||||
import { showSignInDialog } from 'coral-embed-stream/src/actions/auth';
|
||||
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
|
||||
|
||||
/*
|
||||
* Disable false-positive warning below, as it doesn't work well with how we currently
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import get from 'lodash/get';
|
||||
|
||||
export const usernameStatusSelector = state =>
|
||||
get(state, 'auth.user.status.username.status');
|
||||
|
||||
export const usernameSelector = state => get(state, 'auth.user.username');
|
||||
|
||||
export const isLoggedInSelector = state => !!get(state, 'auth.user');
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"server": [
|
||||
"talk-plugin-auth",
|
||||
"talk-plugin-facebook-auth",
|
||||
"talk-plugin-featured-comments",
|
||||
"talk-plugin-offtopic",
|
||||
"talk-plugin-respect"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
const SpamLabel = () => (
|
||||
<CommentDetail
|
||||
icon={'add_box'}
|
||||
icon={'bug_report'}
|
||||
header={t('talk-plugin-akismet.spam_comment')}
|
||||
info={t('talk-plugin-akismet.detected')}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FlagLabel } from 'plugin-api/beta/client/components/ui';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
const SpamLabel = () => (
|
||||
<FlagLabel iconName="add_box">{t('talk-plugin-akismet.spam')}</FlagLabel>
|
||||
<FlagLabel iconName="bug_report">{t('talk-plugin-akismet.spam')}</FlagLabel>
|
||||
);
|
||||
|
||||
export default SpamLabel;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { isSpam } from '../utils';
|
||||
|
||||
const enhance = compose(
|
||||
excludeIf(
|
||||
({ comment: { spam, actions } }) => spam === null || isSpam(actions)
|
||||
({ comment: { spam, actions } }) => spam === null || !isSpam(actions)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { compose } from 'react-apollo';
|
||||
import { excludeIf } from 'plugin-api/beta/client/hocs';
|
||||
import SpamDetail from './SpamDetail';
|
||||
import { isSpam } from '../utils';
|
||||
|
||||
const enhance = compose(
|
||||
excludeIf(
|
||||
({ comment: { spam, actions } }) => spam === null || !isSpam(actions)
|
||||
)
|
||||
);
|
||||
|
||||
export default enhance(SpamDetail);
|
||||
@@ -2,7 +2,6 @@ import translations from './translations.yml';
|
||||
import CheckSpamHook from './containers/CheckSpamHook';
|
||||
import SpamLabel from './containers/SpamLabel';
|
||||
import SpamCommentDetail from './containers/SpamCommentDetail';
|
||||
import SpamCommentFlagDetail from './containers/SpamCommentFlagDetail';
|
||||
|
||||
export default {
|
||||
translations,
|
||||
@@ -10,6 +9,5 @@ export default {
|
||||
commentInputDetailArea: [CheckSpamHook],
|
||||
adminCommentLabels: [SpamLabel],
|
||||
adminCommentMoreDetails: [SpamCommentDetail],
|
||||
adminCommentMoreFlagDetails: [SpamCommentFlagDetail],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -61,18 +61,18 @@ fr:
|
||||
nl_NL:
|
||||
error:
|
||||
COMMENT_IS_SPAM: |
|
||||
The language in this comment looks like spam. You can
|
||||
edit the comment or submit it anyway for moderator review.
|
||||
Deze reactie ziet er uit als spam. Je kan je reactie
|
||||
wijzigen, of indienen zodat ons moderatieteam het kan beoordelen.
|
||||
talk-plugin-akismet:
|
||||
spam: "Spam"
|
||||
spam_comment: "Spam"
|
||||
detected: "Detected by Akismet"
|
||||
detected: "Gedetecteerd door Akismet"
|
||||
still_spam: |
|
||||
Thank you. Our moderation team will review your comment shortly.
|
||||
Dank je wel. Ons moderatieteam zal je reactie beoordelen.
|
||||
flags:
|
||||
reasons:
|
||||
comment:
|
||||
spam_comment: "Detected Spam"
|
||||
spam_comment: "Spam gedetecteerd"
|
||||
pt_BR:
|
||||
error:
|
||||
COMMENT_IS_SPAM: |
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import CreateUsernameDialog from './CreateUsernameDialog';
|
||||
import { withSetUsername } from 'coral-framework/graphql/mutations';
|
||||
import { forEachError } from 'plugin-api/beta/client/utils';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
import {
|
||||
showCreateUsernameDialog,
|
||||
hideCreateUsernameDialog,
|
||||
invalidForm,
|
||||
validForm,
|
||||
updateUsername,
|
||||
} from 'coral-embed-stream/src/actions/auth';
|
||||
|
||||
class ChangeUsernameContainer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
formData: {
|
||||
username: (props.auth.user && props.auth.user.username) || '',
|
||||
},
|
||||
errors: {},
|
||||
showErrors: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
if (
|
||||
!this.props.auth.showCreateUsernameDialog &&
|
||||
next.auth.showCreateUsernameDialog
|
||||
) {
|
||||
this.setState({
|
||||
formData: {
|
||||
username:
|
||||
(this.props.auth.user && this.props.auth.user.username) || '',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = e => {
|
||||
const { name, value } = e.target;
|
||||
this.setState(
|
||||
state => ({
|
||||
...state,
|
||||
formData: {
|
||||
...state.formData,
|
||||
[name]: value,
|
||||
},
|
||||
}),
|
||||
() => {
|
||||
this.validation(name, value);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
addError = (name, error) => {
|
||||
return this.setState(state => ({
|
||||
errors: {
|
||||
...state.errors,
|
||||
[name]: error,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
validation = (name, value) => {
|
||||
const { addError } = this;
|
||||
|
||||
if (!value.length) {
|
||||
addError(name, t('createdisplay.required_field'));
|
||||
} else if (!validate[name](value)) {
|
||||
addError(name, errorMsj[name]);
|
||||
} else {
|
||||
const {[name]: prop, ...errors} = this.state.errors; // eslint-disable-line
|
||||
// Removes Error
|
||||
this.setState(state => ({ ...state, errors }));
|
||||
}
|
||||
};
|
||||
|
||||
isCompleted = () => {
|
||||
const { formData } = this.state;
|
||||
return !Object.keys(formData).filter(prop => !formData[prop].length).length;
|
||||
};
|
||||
|
||||
displayErrors = (show = true) => {
|
||||
this.setState({ showErrors: show });
|
||||
};
|
||||
|
||||
async setUsernameAndClose(username, props = this.props) {
|
||||
const {
|
||||
validForm,
|
||||
invalidForm,
|
||||
setUsername,
|
||||
hideCreateUsernameDialog,
|
||||
updateUsername,
|
||||
} = props;
|
||||
try {
|
||||
// Perform mutation
|
||||
await setUsername(this.props.auth.user.id, username);
|
||||
|
||||
// Also change in redux store...
|
||||
updateUsername(username);
|
||||
|
||||
hideCreateUsernameDialog();
|
||||
validForm();
|
||||
} catch (error) {
|
||||
const msgs = [];
|
||||
forEachError(error, ({ msg }) => msgs.push(msg));
|
||||
invalidForm(t(msgs.join(', ')));
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmitUsername = e => {
|
||||
e.preventDefault();
|
||||
const { errors, formData: { username } } = this.state;
|
||||
const { invalidForm } = this.props;
|
||||
this.displayErrors();
|
||||
if (this.isCompleted() && !Object.keys(errors).length) {
|
||||
this.setUsernameAndClose(username);
|
||||
} else {
|
||||
invalidForm(t('createdisplay.check_the_form'));
|
||||
}
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
this.setUsernameAndClose(this.props.auth.user.username);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loggedIn, auth } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<CreateUsernameDialog
|
||||
open={auth.showCreateUsernameDialog}
|
||||
handleClose={this.handleClose}
|
||||
loggedIn={loggedIn}
|
||||
handleSubmitUsername={this.handleSubmitUsername}
|
||||
{...this}
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChangeUsernameContainer.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
hideCreateUsernameDialog: PropTypes.func,
|
||||
validForm: PropTypes.func,
|
||||
invalidForm: PropTypes.func,
|
||||
loggedIn: PropTypes.bool,
|
||||
changeUsername: PropTypes.func,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ auth }) => ({
|
||||
auth: auth,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
showCreateUsernameDialog,
|
||||
hideCreateUsernameDialog,
|
||||
invalidForm,
|
||||
validForm,
|
||||
updateUsername,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default compose(
|
||||
withSetUsername,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(ChangeUsernameContainer);
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './styles.css';
|
||||
import {
|
||||
Dialog,
|
||||
Alert,
|
||||
TextField,
|
||||
Button,
|
||||
} from 'plugin-api/beta/client/components/ui';
|
||||
import { FakeComment } from './FakeComment';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const CreateUsernameDialog = ({
|
||||
open,
|
||||
handleClose,
|
||||
formData,
|
||||
handleSubmitUsername,
|
||||
handleChange,
|
||||
...props
|
||||
}) => (
|
||||
<Dialog
|
||||
className={styles.dialogusername}
|
||||
id="createUsernameDialog"
|
||||
open={open}
|
||||
>
|
||||
<span className={styles.close} onClick={handleClose}>
|
||||
×
|
||||
</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h1>{t('createdisplay.write_your_username')}</h1>
|
||||
</div>
|
||||
<div>
|
||||
<p className={styles.yourusername}>
|
||||
{t('createdisplay.your_username')}
|
||||
</p>
|
||||
<FakeComment
|
||||
className={styles.fakeComment}
|
||||
username={formData.username}
|
||||
created_at={new Date().toISOString()}
|
||||
body={t('createdisplay.fake_comment_body')}
|
||||
/>
|
||||
<p className={styles.ifyoudont}>
|
||||
{t('createdisplay.if_you_dont_change_your_name')}
|
||||
</p>
|
||||
{props.auth.error && <Alert>{props.auth.error}</Alert>}
|
||||
<form id="saveUsername" onSubmit={handleSubmitUsername}>
|
||||
{props.errors.username && (
|
||||
<span className={styles.hint}>
|
||||
{' '}
|
||||
{t('createdisplay.special_characters')}{' '}
|
||||
</span>
|
||||
)}
|
||||
<div className={styles.saveusername}>
|
||||
<TextField
|
||||
id="username"
|
||||
style={{ fontSize: 16 }}
|
||||
type="string"
|
||||
label={t('createdisplay.username')}
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Button id="save" type="submit" className={styles.saveButton}>
|
||||
{t('createdisplay.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
CreateUsernameDialog.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
handleClose: PropTypes.func,
|
||||
formData: PropTypes.object,
|
||||
handleSubmitUsername: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
auth: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
};
|
||||
|
||||
export default CreateUsernameDialog;
|
||||
@@ -1,81 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './styles.css';
|
||||
import { Button, TextField } from 'plugin-api/beta/client/components/ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class ForgotContent extends React.Component {
|
||||
state = { value: '' };
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.fetchForgotPassword(this.state.value);
|
||||
};
|
||||
|
||||
handleChangeEmail = e => {
|
||||
const { value } = e.target;
|
||||
this.setState({ value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { changeView, auth } = this.props;
|
||||
const { passwordRequestSuccess, passwordRequestFailure } = auth;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h1>{t('sign_in.recover_password')}</h1>
|
||||
</div>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className={styles.textField}>
|
||||
<TextField
|
||||
type="email"
|
||||
style={{ fontSize: 16 }}
|
||||
id="email"
|
||||
name="email"
|
||||
label={t('sign_in.email')}
|
||||
onChange={this.handleChangeEmail}
|
||||
value={this.state.value}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
className={styles.signInButton}
|
||||
full
|
||||
>
|
||||
{t('sign_in.recover_password')}
|
||||
</Button>
|
||||
{passwordRequestSuccess ? (
|
||||
<p className={styles.passwordRequestSuccess}>
|
||||
{passwordRequestSuccess}
|
||||
</p>
|
||||
) : null}
|
||||
{passwordRequestFailure ? (
|
||||
<p className={styles.passwordRequestFailure}>
|
||||
{passwordRequestFailure}
|
||||
</p>
|
||||
) : null}
|
||||
</form>
|
||||
<div className={styles.footer}>
|
||||
<span>
|
||||
{t('sign_in.need_an_account')}{' '}
|
||||
<a onClick={() => changeView('SIGNUP')}>{t('sign_in.register')}</a>
|
||||
</span>
|
||||
<span>
|
||||
{t('sign_in.already_have_an_account')}{' '}
|
||||
<a onClick={() => changeView('SIGNIN')}>{t('sign_in.sign_in')}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ForgotContent.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
changeView: PropTypes.func,
|
||||
fetchForgotPassword: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ForgotContent;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user