Merge branch 'master' into events

This commit is contained in:
Wyatt Johnson
2018-02-16 15:47:37 -07:00
198 changed files with 4263 additions and 3396 deletions
-1
View File
@@ -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": [
+3
View File
@@ -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
View File
@@ -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
//==============================================================================
-181
View File
@@ -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' });
};
-7
View File
@@ -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 });
};
+5 -5
View File
@@ -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 -11
View File
@@ -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,
+8 -4
View File
@@ -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);
+35 -72
View File
@@ -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);
-65
View File
@@ -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;
}
}
-4
View File
@@ -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" />;
+2 -39
View File
@@ -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 */
+110
View File
@@ -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,
});
+6
View File
@@ -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;
+2 -1
View File
@@ -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,
+11
View File
@@ -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`;
+7
View File
@@ -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
);
+93
View File
@@ -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;
});
+124
View File
@@ -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);
+61
View File
@@ -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;
}
}
@@ -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;
+8
View File
@@ -0,0 +1,8 @@
import auth from './auth';
import config from './config';
export default {
auth,
login: auth,
config,
};
+55 -4
View File
@@ -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.
+3 -2
View File
@@ -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 (
+9
View File
@@ -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();
}
+10
View File
@@ -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;
+21
View File
@@ -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";
}
+23
View File
@@ -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;
+1
View File
@@ -10,6 +10,7 @@
line-height: 22px;
min-width: 80px;
text-align: center;
vertical-align: text-top;
}
.icon {
+1
View File
@@ -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';
+4
View File
@@ -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.
+53 -1
View File
@@ -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`)
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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 (
+4 -4
View File
@@ -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);
+2 -2
View File
@@ -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);
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+1
View File
@@ -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: "举报理由(可选)"
+1
View File
@@ -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
View File
@@ -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",
+5
View File
@@ -0,0 +1,5 @@
export {
setAuthToken,
handleSuccessfulLogin,
logout,
} from 'coral-framework/actions/auth';
+1
View File
@@ -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';
+11 -4
View File
@@ -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,
+1 -2
View File
@@ -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
+8
View File
@@ -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
View File
@@ -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