mirror of
https://github.com/wassname/talk.git
synced 2026-09-14 11:36:51 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74d38f16ed | ||
|
|
7129896bd6 | ||
|
|
e2e718494c | ||
|
|
f918bbea20 | ||
|
|
4467f6110f | ||
|
|
a166404347 | ||
|
|
3d2f3ebc59 | ||
|
|
c4cdc672b7 | ||
|
|
b84a488518 | ||
|
|
4b134cbfc2 | ||
|
|
c446098a1e | ||
|
|
b26e938ac5 | ||
|
|
846df0a5e0 | ||
|
|
e7934d9468 | ||
|
|
606d5734f9 | ||
|
|
87ff631eed | ||
|
|
950efba3fe | ||
|
|
af592e326f | ||
|
|
c2aa2077f4 | ||
|
|
26476f42f7 | ||
|
|
3434fb4fed | ||
|
|
a6bc29af9a | ||
|
|
d86ff8418d | ||
|
|
e71bcbe58d | ||
|
|
77c838ea22 | ||
|
|
39313a20fa | ||
|
|
ad4427286d | ||
|
|
dc82db6eda | ||
|
|
4bb2b3d731 | ||
|
|
d812a0f696 | ||
|
|
ba11eb233a | ||
|
|
e76d109f93 | ||
|
|
78db4077e1 | ||
|
|
0519b55707 | ||
|
|
5602bf222c | ||
|
|
bfd6cfbaa4 | ||
|
|
9ec7027f3c | ||
|
|
4acb6aeb6c | ||
|
|
c3bccaf4a8 | ||
|
|
e162a97919 | ||
|
|
f63a7915ea | ||
|
|
18710e2345 | ||
|
|
ff29fa04e5 | ||
|
|
7a9023744a | ||
|
|
b1c03973e6 | ||
|
|
9e4759cbfd | ||
|
|
94c98f000c | ||
|
|
0b2cc29b4a | ||
|
|
0efa419b35 | ||
|
|
c2154b6efa | ||
|
|
cfd52144f4 | ||
|
|
239377dd7f | ||
|
|
c46b29aed9 | ||
|
|
43e02e1574 | ||
|
|
ffd88e0ef0 | ||
|
|
9820e575b2 | ||
|
|
1e329c8967 | ||
|
|
73034a20f0 | ||
|
|
8f7171b54e | ||
|
|
b6e4581ba2 | ||
|
|
78444a0bb4 | ||
|
|
4d7e64e1ec | ||
|
|
b8e340c471 | ||
|
|
39c1fcf83a | ||
|
|
4b2d9fc93a | ||
|
|
f56b8ca3a4 | ||
|
|
0a324a2268 | ||
|
|
5f5f0ffa14 | ||
|
|
fc27e02140 | ||
|
|
f40f209395 | ||
|
|
e924ed9189 | ||
|
|
4053a25239 | ||
|
|
de094ee948 | ||
|
|
aecae3efbe | ||
|
|
67ade3a323 | ||
|
|
6d2e74eeec | ||
|
|
c7ee8307e6 | ||
|
|
eaeb7ea0da | ||
|
|
9e28be8246 | ||
|
|
431bbd4f03 | ||
|
|
0f69df26f6 | ||
|
|
c140a62822 | ||
|
|
cad25eedd1 | ||
|
|
87af05c0d9 | ||
|
|
c54a8bdc4f | ||
|
|
40963e236f | ||
|
|
d857585c7f | ||
|
|
3999b806c7 | ||
|
|
6c1ed826d0 | ||
|
|
4224f18ba7 | ||
|
|
6feaa94fba | ||
|
|
d639596047 | ||
|
|
f5409b11bf | ||
|
|
a0118751c7 | ||
|
|
6860b2b03f | ||
|
|
ec8cfdf5f7 | ||
|
|
142c5b32f1 | ||
|
|
2bca30d3c1 | ||
|
|
3dde285776 | ||
|
|
1304669416 | ||
|
|
9703bc8645 | ||
|
|
85d67dba19 | ||
|
|
ffc880c33b | ||
|
|
ae881df7f3 | ||
|
|
33dd2e5c0a | ||
|
|
dbe471fa96 | ||
|
|
afe411515d | ||
|
|
a214969527 | ||
|
|
bd16ff863d | ||
|
|
c0b0627d6a | ||
|
|
13a52e8d64 | ||
|
|
968dd14720 | ||
|
|
065af06709 | ||
|
|
b2ce217a41 | ||
|
|
e93e4756bf | ||
|
|
349c972f8b | ||
|
|
f515e22217 | ||
|
|
90c99727cb | ||
|
|
54ce58c48f | ||
|
|
175b677afd | ||
|
|
39b34d624a | ||
|
|
e9a781607a | ||
|
|
05a6af49c4 | ||
|
|
4b2aa04ef6 | ||
|
|
5481491bf2 | ||
|
|
eef55f33fc | ||
|
|
2ad5219e59 | ||
|
|
e08024a136 | ||
|
|
9f465ee7ce | ||
|
|
29a64d540b | ||
|
|
749189f0e5 | ||
|
|
41413a9e91 | ||
|
|
e65c71cae7 | ||
|
|
c7775cfdfd | ||
|
|
78473cb21f | ||
|
|
4a3bdb0633 | ||
|
|
fdafa1ca42 | ||
|
|
243f846f3b | ||
|
|
8f72ff7ca2 | ||
|
|
adf0b65fde | ||
|
|
3bb0cb9168 | ||
|
|
670084427d | ||
|
|
4b6a3b5b9d | ||
|
|
c8727e0f41 | ||
|
|
705c54f397 | ||
|
|
f4fc8915e3 | ||
|
|
1b2929af29 | ||
|
|
1774f98519 | ||
|
|
4883bb17b9 | ||
|
|
2f71e1bf8e | ||
|
|
408a082f2d | ||
|
|
37dd6ae044 | ||
|
|
2e0a4df2be | ||
|
|
6965b33968 | ||
|
|
d2235f9918 | ||
|
|
5ebb99740b | ||
|
|
2978d6138c | ||
|
|
4dbd9e7df1 | ||
|
|
686096bfae | ||
|
|
ddb80a95c1 | ||
|
|
18cc8b9389 | ||
|
|
22467ac123 | ||
|
|
d526a404ae | ||
|
|
aaad7aa86f | ||
|
|
74a2ed4a41 | ||
|
|
bad0363628 | ||
|
|
ffbcd93c3f | ||
|
|
2407cb9594 | ||
|
|
61bc372640 | ||
|
|
ee3d576a49 | ||
|
|
3c04e6c7ec | ||
|
|
52491d81a9 | ||
|
|
77723e3fb3 | ||
|
|
bba14f502a | ||
|
|
44913864eb | ||
|
|
3c66401546 | ||
|
|
2e6a7c0559 | ||
|
|
e7fc33c667 | ||
|
|
1cc484b61e | ||
|
|
17fefcb4bd | ||
|
|
f4d520fbaa | ||
|
|
f8e0556904 | ||
|
|
a5c08129ce | ||
|
|
7c859ad4c3 | ||
|
|
2b2a762623 | ||
|
|
b9cfddc5c0 | ||
|
|
7dca937de2 | ||
|
|
6cb3fc7be8 | ||
|
|
95211e6cb9 | ||
|
|
db322d192f | ||
|
|
bf98050d33 | ||
|
|
caa588f11d | ||
|
|
764ba4ab36 | ||
|
|
9c139eb703 | ||
|
|
c289ed149f | ||
|
|
05d80a0549 | ||
|
|
8bef21274d | ||
|
|
6d662e4469 | ||
|
|
9debb0d48d | ||
|
|
484ae2539b | ||
|
|
f75924e683 | ||
|
|
0966aa9463 | ||
|
|
30cd16fd56 | ||
|
|
4464183b42 | ||
|
|
ed8d9e985c | ||
|
|
840fef84db | ||
|
|
894141619c | ||
|
|
7e135cef47 | ||
|
|
34726e2c4e | ||
|
|
dc5a3e43a6 | ||
|
|
2e59f64738 | ||
|
|
91e08cb117 | ||
|
|
118301c2b4 |
+24
-22
@@ -1,32 +1,34 @@
|
||||
**/*.html
|
||||
dist
|
||||
docs
|
||||
**/*.html
|
||||
node_modules
|
||||
plugins/*
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
public
|
||||
|
||||
!plugins/talk-plugin-akismet
|
||||
!plugins/talk-plugin-auth
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-mod
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-viewing-options
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-flag-details
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-mod
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-flag-details
|
||||
public
|
||||
node_modules
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-viewing-options
|
||||
@@ -23,6 +23,7 @@ browserstack.err
|
||||
|
||||
plugins.json
|
||||
plugins/*
|
||||
!plugins/talk-plugin-akismet
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-auth
|
||||
!plugins/talk-plugin-respect
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"exec": "npm-run-all --parallel generate-introspection start:development",
|
||||
"verbose": true,
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
|
||||
"ext": "js,json,graphql,yml",
|
||||
"watch": [
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
FROM coralproject/talk:latest
|
||||
|
||||
# Setup the build arguments
|
||||
ONBUILD ARG TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10
|
||||
ONBUILD ARG TALK_ASSET_COMMENTS_LOAD_DEPTH=10
|
||||
ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
|
||||
ONBUILD ARG TALK_THREADING_LEVEL=3
|
||||
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
|
||||
ONBUILD ARG TALK_DEFAULT_LANG=en
|
||||
|
||||
@@ -5,7 +5,6 @@ const uuid = require('uuid');
|
||||
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');
|
||||
@@ -72,9 +71,6 @@ app.use(
|
||||
)
|
||||
);
|
||||
|
||||
// Compress the responses if appropriate.
|
||||
app.use(compression());
|
||||
|
||||
//==============================================================================
|
||||
// VIEW CONFIGURATION
|
||||
//==============================================================================
|
||||
|
||||
@@ -1,177 +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, storage }
|
||||
) => {
|
||||
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 && storage) {
|
||||
storage.removeItem('token');
|
||||
storage.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, storage }) => {
|
||||
dispatch(checkLoginRequest());
|
||||
return rest('/auth')
|
||||
.then(({ user }) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios && storage) {
|
||||
storage.removeItem('token');
|
||||
storage.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, storage }) => {
|
||||
return rest('/auth', { method: 'DELETE' }).then(() => {
|
||||
if (storage) {
|
||||
storage.removeItem('token');
|
||||
storage.removeItem('exp');
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({ type: actions.LOGOUT });
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// AUTH TOKEN
|
||||
//==============================================================================
|
||||
|
||||
export const handleAuthToken = token => (dispatch, _, { storage }) => {
|
||||
if (storage) {
|
||||
storage.setItem('exp', jwtDecode(token).exp);
|
||||
storage.setItem('token', token);
|
||||
}
|
||||
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
|
||||
|
||||
export const fetchConfig = () => dispatch => {
|
||||
let json = document.getElementById('data');
|
||||
let data = JSON.parse(json.textContent);
|
||||
dispatch({ type: CONFIG_UPDATED, data });
|
||||
};
|
||||
@@ -4,10 +4,10 @@ export const toggleModal = open => ({ type: actions.TOGGLE_MODAL, open });
|
||||
export const singleView = () => ({ type: actions.SINGLE_VIEW });
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => (dispatch, _, { storage }) => {
|
||||
export const hideShortcutsNote = () => (dispatch, _, { localStorage }) => {
|
||||
try {
|
||||
if (storage) {
|
||||
storage.setItem('coral:shortcutsNote', 'hide');
|
||||
if (localStorage) {
|
||||
localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
}
|
||||
} catch (e) {
|
||||
// above will fail in Safari private mode
|
||||
|
||||
@@ -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"
|
||||
@@ -44,7 +44,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)}
|
||||
<a onClick={handleLogout}>Sign Out</a>
|
||||
<a onClick={handleLogout}>{t('configure.sign_out')}</a>
|
||||
<span>{`v${process.env.VERSION}`}</span>
|
||||
</Navigation>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
CoralDrawer.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool, // hide app elements from a logged out user
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
};
|
||||
|
||||
export default CoralDrawer;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
.header, .cta, .success {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.success {
|
||||
cursor: pointer;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
.signInLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.signInLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ForgotPassword.css';
|
||||
import { Button, TextField, Alert, Success } from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class ForgotPassword extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleEmailChange = e => this.props.onEmailChange(e.target.value);
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleSignInLink = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSignInLink();
|
||||
};
|
||||
|
||||
renderSuccess() {
|
||||
return (
|
||||
<div className={styles.success} onClick={this.handleSignInLink}>
|
||||
{t('password_reset.mail_sent')}{' '}
|
||||
<a
|
||||
className={styles.signInLink}
|
||||
href="#"
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
<Success />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
const { email, errorMessage } = this.props;
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<Button type="submit" cStyle="black" full>
|
||||
Reset Password
|
||||
</Button>
|
||||
<p className={styles.cta}>
|
||||
Go back to{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.signInLink}
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign In
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.success ? this.renderSuccess() : this.renderForm();
|
||||
}
|
||||
}
|
||||
|
||||
ForgotPassword.propTypes = {
|
||||
success: PropTypes.bool.isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
onEmailChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
@@ -13,7 +13,7 @@ import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
const CoralHeader = ({
|
||||
handleLogout,
|
||||
showShortcuts = () => {},
|
||||
auth,
|
||||
currentUser,
|
||||
root,
|
||||
data,
|
||||
}) => {
|
||||
@@ -22,9 +22,9 @@ const CoralHeader = ({
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
|
||||
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
<Navigation className={styles.nav}>
|
||||
{can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
{can(currentUser, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
id="moderateNav"
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
@@ -54,7 +54,7 @@ const CoralHeader = ({
|
||||
<CommunityIndicator root={root} data={data} />
|
||||
</Link>
|
||||
|
||||
{can(auth.user, 'UPDATE_CONFIG') && (
|
||||
{can(currentUser, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id="configureNav"
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
@@ -97,12 +97,14 @@ const CoralHeader = ({
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
className="talk-admin-header-sign-out"
|
||||
>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
{currentUser && (
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
className="talk-admin-header-sign-out"
|
||||
>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
@@ -116,7 +118,7 @@ const CoralHeader = ({
|
||||
};
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
|
||||
@@ -10,22 +10,26 @@ const Layout = ({
|
||||
handleLogout = () => {},
|
||||
toggleShortcutModal = () => {},
|
||||
restricted = false,
|
||||
auth,
|
||||
currentUser,
|
||||
}) => (
|
||||
<LayoutMDL className={styles.layout} fixedDrawer>
|
||||
<Header
|
||||
handleLogout={handleLogout}
|
||||
showShortcuts={toggleShortcutModal}
|
||||
auth={auth}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
<Drawer
|
||||
handleLogout={handleLogout}
|
||||
restricted={restricted}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} auth={auth} />
|
||||
<div className={styles.layout}>{children}</div>
|
||||
</LayoutMDL>
|
||||
);
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
restricted: PropTypes.bool, // hide elements from a user that's logged out
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.layout {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header, .cta {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
import SignIn from '../containers/SignIn';
|
||||
import ForgotPassword from '../containers/ForgotPassword';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Login.css';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
import cn from 'classnames';
|
||||
|
||||
class LoginContainer extends Component {
|
||||
renderForm() {
|
||||
return this.props.forgotPassword ? (
|
||||
<ForgotPassword onSignInLink={this.props.onSignInLink} />
|
||||
) : (
|
||||
<SignIn onForgotPasswordLink={this.props.onForgotPasswordLink} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={cn(styles.layout, 'talk-admin-login')}>
|
||||
<h1 className={styles.header}>Team sign in</h1>
|
||||
<p className={styles.cta}>Sign in to interact with your community.</p>
|
||||
{this.renderForm()}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginContainer.propTypes = {
|
||||
forgotPassword: PropTypes.bool.isRequired,
|
||||
onForgotPasswordLink: PropTypes.func.isRequired,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default LoginContainer;
|
||||
@@ -3,35 +3,3 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loginLayout {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loginHeader, .loginCTA, .forgotPasswordCTA, .passwordRequestSuccess {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.forgotPasswordLink, .signInLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgotPasswordLink:hover, .signInLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.loginHeader {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
cursor: pointer;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.forgotPasswordCTA {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.forgotPasswordLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.forgotPasswordLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.recaptcha {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './SignIn.css';
|
||||
import { Button, TextField, Alert } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import Recaptcha from 'coral-framework/components/Recaptcha';
|
||||
|
||||
class SignIn extends React.Component {
|
||||
recaptcha = null;
|
||||
|
||||
handleForgotPasswordLink = e => {
|
||||
e.preventDefault();
|
||||
this.props.onForgotPasswordLink();
|
||||
};
|
||||
handleEmailChange = e => this.props.onEmailChange(e.target.value);
|
||||
handlePasswordChange = e => this.props.onPasswordChange(e.target.value);
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
|
||||
// Reset recaptcha because each response can only
|
||||
// be used once.
|
||||
if (this.recaptcha) {
|
||||
this.recaptcha.reset();
|
||||
}
|
||||
};
|
||||
|
||||
handleRecaptchaRef = ref => {
|
||||
this.recaptcha = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { email, password, errorMessage, requireRecaptcha } = this.props;
|
||||
return (
|
||||
<form className="talk-admin-login-sign-in" onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={this.handlePasswordChange}
|
||||
type="password"
|
||||
/>
|
||||
{requireRecaptcha && (
|
||||
<div className={styles.recaptcha}>
|
||||
<Recaptcha
|
||||
ref={this.handleRecaptchaRef}
|
||||
onVerify={this.props.onRecaptchaVerify}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className={cn(styles.signInButton, 'talk-admin-login-sign-in-button')}
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
full
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password?{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
>
|
||||
Request a new one.
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SignIn.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
password: PropTypes.string.isRequired,
|
||||
onEmailChange: PropTypes.func.isRequired,
|
||||
onPasswordChange: PropTypes.func.isRequired,
|
||||
onForgotPasswordLink: PropTypes.func.isRequired,
|
||||
onRecaptchaVerify: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
requireRecaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import UserInfoTooltip from './UserInfoTooltip';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class UserDetail extends React.Component {
|
||||
rejectThenReload = async info => {
|
||||
@@ -152,27 +153,27 @@ class UserDetail extends React.Component {
|
||||
>
|
||||
{suspended ? (
|
||||
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
|
||||
Remove Suspension
|
||||
{t('user_detail.remove_suspension')}
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
disabled={me.id === user.id}
|
||||
onClick={this.showSuspenUserDialog}
|
||||
>
|
||||
Suspend User
|
||||
{t('user_detail.suspend')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
{banned ? (
|
||||
<ActionsMenuItem onClick={() => unbanUser({ id: user.id })}>
|
||||
Remove Ban
|
||||
{t('user_detail.remove_ban')}
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
disabled={me.id === user.id}
|
||||
onClick={this.showBanUserDialog}
|
||||
>
|
||||
Ban User
|
||||
{t('user_detail.ban')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
</ActionsMenu>
|
||||
@@ -190,14 +191,18 @@ class UserDetail extends React.Component {
|
||||
<ul className={styles.userDetailList}>
|
||||
<li>
|
||||
<Icon name="assignment_ind" />
|
||||
<span className={styles.userDetailItem}>Member Since:</span>
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.member_since')}:
|
||||
</span>
|
||||
{new Date(user.created_at).toLocaleString()}
|
||||
</li>
|
||||
|
||||
{user.profiles.map(({ id }) => (
|
||||
<li key={id}>
|
||||
<Icon name="email" />
|
||||
<span className={styles.userDetailItem}>Email:</span>
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.email')}:
|
||||
</span>
|
||||
{id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
@@ -210,17 +215,23 @@ class UserDetail extends React.Component {
|
||||
|
||||
<ul className={styles.stats}>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>Total Comments</span>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.total_comments')}
|
||||
</span>
|
||||
<span className={styles.statResult}>{totalComments}</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>Reject Rate</span>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.reject_rate')}
|
||||
</span>
|
||||
<span className={styles.statResult}>
|
||||
{rejectedPercent.toFixed(1)}%
|
||||
</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>Reports</span>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.reports')}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
styles.statReportResult,
|
||||
@@ -259,13 +270,13 @@ class UserDetail extends React.Component {
|
||||
'talk-admin-user-detail-all-tab'
|
||||
)}
|
||||
>
|
||||
All
|
||||
{t('user_detail.all')}
|
||||
</Tab>
|
||||
<Tab
|
||||
tabId={'rejected'}
|
||||
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')}
|
||||
>
|
||||
Rejected
|
||||
{t('user_detail.rejected')}
|
||||
</Tab>
|
||||
<Tab
|
||||
tabId={'history'}
|
||||
@@ -275,7 +286,7 @@ class UserDetail extends React.Component {
|
||||
'talk-admin-user-detail-history-tab'
|
||||
)}
|
||||
>
|
||||
Account History
|
||||
{t('user_detail.account_history')}
|
||||
</Tab>
|
||||
</TabBar>
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
|
||||
z-index: 10;
|
||||
top: 32px;
|
||||
right: 0px;
|
||||
width: 140px;
|
||||
left: -100px;
|
||||
width: 200px;
|
||||
text-align: left;
|
||||
color: #616161;
|
||||
}
|
||||
@@ -39,7 +39,7 @@
|
||||
border: 10px solid transparent;
|
||||
border-top-color: #999;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
left: 96px;
|
||||
top: -20px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -49,7 +49,7 @@
|
||||
border: 10px solid transparent;
|
||||
border-top-color: white;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
left: 96px;
|
||||
top: -19px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class UserInfoTooltip extends React.Component {
|
||||
new Date(
|
||||
this.getLastHistoryItem(user, 'banned').created_at
|
||||
)
|
||||
).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
).format('MMM Do YYYY, h:mm:ss a')}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
@@ -139,7 +139,7 @@ class UserInfoTooltip extends React.Component {
|
||||
'suspension'
|
||||
).created_at
|
||||
)
|
||||
).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
).format('MMM Do YYYY, h:mm:ss a')}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
@@ -154,7 +154,7 @@ class UserInfoTooltip extends React.Component {
|
||||
new Date(
|
||||
this.getLastHistoryItem(user, 'suspension').until
|
||||
)
|
||||
).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
).format('MMM Do YYYY, h:mm:ss a')}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withForgotPassword } from 'coral-framework/hocs';
|
||||
import { compose } from 'recompose';
|
||||
import ForgotPassword from '../components/ForgotPassword';
|
||||
|
||||
class ForgotPasswordContainer extends Component {
|
||||
state = {
|
||||
email: '',
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.forgotPassword(this.state.email);
|
||||
};
|
||||
|
||||
handleEmailChange = email => {
|
||||
this.setState({ email });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ForgotPassword
|
||||
onSubmit={this.handleSubmit}
|
||||
onEmailChange={this.handleEmailChange}
|
||||
email={this.state.email}
|
||||
errorMessage={this.props.errorMessage}
|
||||
success={this.props.success}
|
||||
onSignInLink={this.props.onSignInLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ForgotPasswordContainer.propTypes = {
|
||||
success: PropTypes.bool.isRequired,
|
||||
forgotPassword: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default compose(withForgotPassword)(ForgotPasswordContainer);
|
||||
@@ -2,86 +2,58 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import Layout from '../components/Layout';
|
||||
import { fetchConfig } from '../actions/config';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
import Login from '../containers/Login';
|
||||
import { FullLoading } from '../components/FullLoading';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
|
||||
import {
|
||||
checkLogin,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
logout,
|
||||
} from '../actions/auth';
|
||||
import { logout } from 'coral-framework/actions/auth';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import UserDetail from 'coral-admin/src/containers/UserDetail';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class LayoutContainer extends React.Component {
|
||||
componentWillMount() {
|
||||
const { checkLogin, fetchConfig } = this.props;
|
||||
|
||||
checkLogin();
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
loggedIn,
|
||||
loadingUser,
|
||||
loginError,
|
||||
loginMaxExceeded,
|
||||
passwordRequestSuccess,
|
||||
} = this.props.auth;
|
||||
|
||||
const {
|
||||
currentUser,
|
||||
checkedInitialLogin,
|
||||
children,
|
||||
logout,
|
||||
toggleShortcutModal,
|
||||
TALK_RECAPTCHA_PUBLIC,
|
||||
} = this.props;
|
||||
|
||||
if (loadingUser) {
|
||||
if (!checkedInitialLogin) {
|
||||
return <FullLoading />;
|
||||
}
|
||||
|
||||
if (!loggedIn) {
|
||||
return (
|
||||
<AdminLogin
|
||||
loginMaxExceeded={loginMaxExceeded}
|
||||
handleLogin={this.props.handleLogin}
|
||||
requestPasswordReset={this.props.requestPasswordReset}
|
||||
passwordRequestSuccess={passwordRequestSuccess}
|
||||
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
|
||||
errorMessage={loginError}
|
||||
/>
|
||||
);
|
||||
if (!currentUser) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
if (can(user, 'ACCESS_ADMIN') && loggedIn) {
|
||||
return (
|
||||
<Layout
|
||||
handleLogout={logout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
auth={this.props.auth}
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
return (
|
||||
<Layout {...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);
|
||||
@@ -14,8 +14,8 @@ import { hideShortcutsNote } from './actions/moderation';
|
||||
|
||||
smoothscroll.polyfill();
|
||||
|
||||
function init({ store, storage }) {
|
||||
if (storage && storage.getItem('coral:shortcutsNote') === 'hide') {
|
||||
function init({ store, localStorage }) {
|
||||
if (localStorage && localStorage.getItem('coral:shortcutsNote') === 'hide') {
|
||||
store.dispatch(hideShortcutsNote());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import * as actions from '../constants/auth';
|
||||
|
||||
const initialState = {
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
loginError: null,
|
||||
loginMaxExceeded: false,
|
||||
passwordRequestSuccess: null,
|
||||
};
|
||||
|
||||
export default function auth(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CHECK_LOGIN_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
loadingUser: true,
|
||||
};
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loggedIn: false,
|
||||
loadingUser: false,
|
||||
user: null,
|
||||
};
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loggedIn: true,
|
||||
loadingUser: false,
|
||||
user: action.user,
|
||||
};
|
||||
case actions.LOGOUT:
|
||||
return initialState;
|
||||
case actions.LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loginMaxExceeded: false,
|
||||
loginError: null,
|
||||
};
|
||||
case actions.LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loginError: action.message,
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
passwordRequestSuccess: null,
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
passwordRequestSuccess:
|
||||
'If you have a registered account, a password reset link was sent to that email.',
|
||||
};
|
||||
case actions.LOGIN_MAXIMUM_EXCEEDED:
|
||||
return {
|
||||
...state,
|
||||
loginMaxExceeded: true,
|
||||
loginError: action.message,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
import auth from './auth';
|
||||
import stories from './stories';
|
||||
import configure from './configure';
|
||||
import community from './community';
|
||||
import moderation from './moderation';
|
||||
import install from './install';
|
||||
import config from './config';
|
||||
import banUserDialog from './banUserDialog';
|
||||
import suspendUserDialog from './suspendUserDialog';
|
||||
import userDetail from './userDetail';
|
||||
import ui from './ui';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
banUserDialog,
|
||||
configure,
|
||||
suspendUserDialog,
|
||||
@@ -20,6 +17,5 @@ export default {
|
||||
community,
|
||||
moderation,
|
||||
install,
|
||||
config,
|
||||
ui,
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class Configure extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
auth: { user },
|
||||
currentUser,
|
||||
canSave,
|
||||
savePending,
|
||||
setActiveSection,
|
||||
@@ -32,7 +32,7 @@ export default class Configure extends Component {
|
||||
} = this.props;
|
||||
const SectionComponent = this.getSectionComponent(activeSection);
|
||||
|
||||
if (!can(user, 'UPDATE_CONFIG')) {
|
||||
if (!can(currentUser, 'UPDATE_CONFIG')) {
|
||||
return (
|
||||
<p>
|
||||
You must be an administrator to access config settings. Please find
|
||||
@@ -87,7 +87,7 @@ export default class Configure extends Component {
|
||||
|
||||
Configure.propTypes = {
|
||||
savePending: PropTypes.func.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
|
||||
@@ -30,7 +30,7 @@ class ConfigureContainer extends Component {
|
||||
|
||||
return (
|
||||
<Configure
|
||||
auth={this.props.auth}
|
||||
currentUser={this.props.currentUser}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
settings={this.props.mergedSettings}
|
||||
@@ -71,7 +71,7 @@ const withConfigureQuery = withQuery(
|
||||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth,
|
||||
currentUser: state.auth.user,
|
||||
pending: state.configure.pending,
|
||||
canSave: state.configure.canSave,
|
||||
activeSection: state.configure.activeSection,
|
||||
@@ -97,7 +97,7 @@ ConfigureContainer.propTypes = {
|
||||
updateSettings: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
setActiveSection: PropTypes.func.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -143,7 +143,6 @@
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
top: 2px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ class Moderation extends Component {
|
||||
commentBelongToQueue={this.props.commentBelongToQueue}
|
||||
isLoadingMore={this.state.isLoadingMore}
|
||||
commentCount={activeTabCount}
|
||||
currentUserId={this.props.auth.user.id}
|
||||
currentUserId={this.props.currentUser.id}
|
||||
viewUserDetail={viewUserDetail}
|
||||
selectCommentId={props.selectCommentId}
|
||||
cleanUpQueue={props.cleanUpQueue}
|
||||
@@ -225,7 +225,7 @@ Moderation.propTypes = {
|
||||
cleanUpQueue: PropTypes.func.isRequired,
|
||||
storySearchChange: PropTypes.func.isRequired,
|
||||
moderation: PropTypes.object.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
queueConfig: PropTypes.object.isRequired,
|
||||
commentBelongToQueue: PropTypes.func.isRequired,
|
||||
handleCommentChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -19,12 +19,12 @@ class ViewOptions extends React.Component {
|
||||
'talk-admin-moderation-view-options-headline'
|
||||
)}
|
||||
>
|
||||
View Options
|
||||
{t('admin_sidebar.view_options')}
|
||||
</h2>
|
||||
<div className={styles.viewOptionsContent}>
|
||||
<ul className={styles.viewOptionsList}>
|
||||
<li className={styles.viewOptionsItem}>
|
||||
Sort Comments
|
||||
{t('admin_sidebar.sort_comments')}
|
||||
<Dropdown
|
||||
containerClassName={styles.dropdownContainer}
|
||||
toggleClassName={styles.dropdownToggle}
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -407,6 +407,7 @@ export const subscriptionFields = `
|
||||
type
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
created_at
|
||||
}
|
||||
|
||||
@@ -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, _, { storage }) => {
|
||||
if (storage) {
|
||||
storage.setItem('exp', jwtDecode(token).exp);
|
||||
storage.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, storage }
|
||||
) => {
|
||||
await rest('/auth', { method: 'DELETE' });
|
||||
|
||||
if (storage) {
|
||||
storage.removeItem('token');
|
||||
storage.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, storage }
|
||||
) => {
|
||||
dispatch(checkLoginRequest());
|
||||
rest('/auth')
|
||||
.then(result => {
|
||||
if (!result.user) {
|
||||
if (storage) {
|
||||
storage.removeItem('token');
|
||||
storage.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 && storage) {
|
||||
// Unauthorized.
|
||||
storage.removeItem('token');
|
||||
storage.removeItem('exp');
|
||||
}
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
export const validForm = () => ({ type: actions.VALID_FORM });
|
||||
export const invalidForm = error => ({ type: actions.INVALID_FORM, error });
|
||||
|
||||
//==============================================================================
|
||||
// VERIFY EMAIL
|
||||
//==============================================================================
|
||||
|
||||
const verifyEmailRequest = () => ({
|
||||
type: actions.VERIFY_EMAIL_REQUEST,
|
||||
});
|
||||
|
||||
const verifyEmailSuccess = () => ({
|
||||
type: actions.VERIFY_EMAIL_SUCCESS,
|
||||
});
|
||||
|
||||
const verifyEmailFailure = error => ({
|
||||
type: actions.VERIFY_EMAIL_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestConfirmEmail = email => (dispatch, getState, { rest }) => {
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
dispatch(verifyEmailRequest());
|
||||
return rest('/users/resend-verify', {
|
||||
method: 'POST',
|
||||
body: { email },
|
||||
headers: { 'X-Pym-Url': redirectUri },
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(verifyEmailSuccess());
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(verifyEmailFailure(error));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// Login Popup actions.
|
||||
export const setRequireEmailVerification = required => ({
|
||||
type: actions.SET_REQUIRE_EMAIL_VERIFICATION,
|
||||
required,
|
||||
});
|
||||
|
||||
export const setRedirectUri = uri => ({
|
||||
type: actions.SET_REDIRECT_URI,
|
||||
uri,
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// Edit Username
|
||||
//==============================================================================
|
||||
|
||||
const editUsernameFailure = error => ({
|
||||
type: actions.EDIT_USERNAME_FAILURE,
|
||||
error,
|
||||
});
|
||||
const editUsernameSuccess = () => ({ type: actions.EDIT_USERNAME_SUCCESS });
|
||||
|
||||
export const editName = username => (dispatch, _, { rest }) => {
|
||||
return rest('/account/username', { method: 'PUT', body: { username } })
|
||||
.then(() => {
|
||||
dispatch(editUsernameSuccess());
|
||||
dispatch(notify('success', t('framework.success_name_update')));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(editUsernameFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as actions from '../constants/login';
|
||||
import { checkLogin } from 'coral-framework/actions/auth';
|
||||
|
||||
export const showSignInDialog = () => ({
|
||||
type: actions.SHOW_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const hideSignInDialog = () => dispatch => {
|
||||
dispatch(checkLogin());
|
||||
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
|
||||
};
|
||||
|
||||
export const focusSignInDialog = () => ({
|
||||
type: actions.FOCUS_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const blurSignInDialog = () => ({
|
||||
type: actions.BLUR_SIGNIN_DIALOG,
|
||||
});
|
||||
@@ -16,17 +16,10 @@ import cn from 'classnames';
|
||||
|
||||
export default class Embed extends React.Component {
|
||||
changeTab = tab => {
|
||||
// TODO: move data fetching to appropiate containers.
|
||||
switch (tab) {
|
||||
case 'profile':
|
||||
this.props.data.refetch();
|
||||
break;
|
||||
}
|
||||
this.props.setActiveTab(tab);
|
||||
};
|
||||
|
||||
getTabs() {
|
||||
const { user } = this.props.auth;
|
||||
const tabs = [
|
||||
<Tab
|
||||
key="stream"
|
||||
@@ -43,7 +36,7 @@ export default class Embed extends React.Component {
|
||||
{t('framework.my_profile')}
|
||||
</Tab>,
|
||||
];
|
||||
if (can(user, 'UPDATE_ASSET_CONFIG')) {
|
||||
if (can(this.props.currentUser, 'UPDATE_ASSET_CONFIG')) {
|
||||
tabs.push(
|
||||
<Tab
|
||||
key="config"
|
||||
@@ -64,11 +57,12 @@ export default class Embed extends React.Component {
|
||||
root,
|
||||
root: { asset },
|
||||
data,
|
||||
auth: { showSignInDialog, signInDialogFocus },
|
||||
showSignInDialog,
|
||||
signInDialogFocus,
|
||||
blurSignInDialog,
|
||||
focusSignInDialog,
|
||||
hideSignInDialog,
|
||||
router: { location: { query: { parentUrl } } },
|
||||
parentUrl,
|
||||
} = this.props;
|
||||
const hasHighlightedComment = !!commentId;
|
||||
|
||||
@@ -81,9 +75,7 @@ export default class Embed extends React.Component {
|
||||
<AutomaticAssetClosure asset={asset} />
|
||||
<IfSlotIsNotEmpty slot="login">
|
||||
<Popup
|
||||
href={`embed/stream/login?parentUrl=${encodeURIComponent(
|
||||
parentUrl
|
||||
)}`}
|
||||
href={`login?parentUrl=${encodeURIComponent(parentUrl)}`}
|
||||
title="Login"
|
||||
features="menubar=0,resizable=0,width=500,height=550,top=200,left=500"
|
||||
open={showSignInDialog}
|
||||
@@ -138,11 +130,13 @@ export default class Embed extends React.Component {
|
||||
|
||||
Embed.propTypes = {
|
||||
setActiveTab: PropTypes.func,
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
showSignInDialog: PropTypes.bool,
|
||||
signInDialogFocus: PropTypes.bool,
|
||||
blurSignInDialog: PropTypes.func,
|
||||
focusSignInDialog: PropTypes.func,
|
||||
hideSignInDialog: PropTypes.func,
|
||||
router: PropTypes.object,
|
||||
parentUrl: PropTypes.string,
|
||||
commentId: PropTypes.string,
|
||||
root: PropTypes.object,
|
||||
activeTab: PropTypes.string,
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
export const CHANGE_VIEW = 'CHANGE_VIEW';
|
||||
export const CLEAN_STATE = 'CLEAN_STATE';
|
||||
|
||||
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
|
||||
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
|
||||
export const FOCUS_SIGNIN_DIALOG = 'FOCUS_SIGNIN_DIALOG';
|
||||
export const BLUR_SIGNIN_DIALOG = 'BLUR_SIGNIN_DIALOG';
|
||||
|
||||
export const CREATE_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST';
|
||||
export const CREATE_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS';
|
||||
export const CREATE_USERNAME_FAILURE = 'CREATE_USERNAME_FAILURE';
|
||||
export const CREATE_USERNAME = 'CREATE_USERNAME';
|
||||
export const SHOW_CREATEUSERNAME_DIALOG = 'SHOW_CREATEUSERNAME_DIALOG';
|
||||
export const HIDE_CREATEUSERNAME_DIALOG = 'HIDE_CREATEUSERNAME_DIALOG';
|
||||
|
||||
export const EDIT_USERNAME_REQUEST = 'CREATE_USERNAME_REQUEST';
|
||||
export const EDIT_USERNAME_SUCCESS = 'CREATE_USERNAME_SUCCESS';
|
||||
export const EDIT_USERNAME_FAILURE = 'CREATE_USERNAME_FAILURE';
|
||||
export const EDIT_USERNAME = 'CREATE_USERNAME';
|
||||
|
||||
export const FETCH_SIGNUP_REQUEST = 'FETCH_SIGNUP_REQUEST';
|
||||
export const FETCH_SIGNUP_FAILURE = 'FETCH_SIGNUP_FAILURE';
|
||||
export const FETCH_SIGNUP_SUCCESS = 'FETCH_SIGNUP_SUCCESS';
|
||||
|
||||
export const FETCH_SIGNIN_REQUEST = 'FETCH_SIGNIN_REQUEST';
|
||||
export const FETCH_SIGNIN_FAILURE = 'FETCH_SIGNIN_FAILURE';
|
||||
export const FETCH_SIGNIN_SUCCESS = 'FETCH_SIGNIN_SUCCESS';
|
||||
|
||||
export const FETCH_SIGNIN_FACEBOOK_REQUEST = 'FETCH_SIGNIN_FACEBOOK_REQUEST';
|
||||
export const FETCH_SIGNIN_FACEBOOK_FAILURE = 'FETCH_SIGNIN_FACEBOOK_FAILURE';
|
||||
export const FETCH_SIGNIN_FACEBOOK_SUCCESS = 'FETCH_SIGNIN_FACEBOOK_SUCCESS';
|
||||
|
||||
export const FETCH_SIGNUP_FACEBOOK_REQUEST = 'FETCH_SIGNUP_FACEBOOK_REQUEST';
|
||||
export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
|
||||
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
|
||||
export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE';
|
||||
|
||||
export const LOGOUT = 'LOGOUT';
|
||||
|
||||
export const INVALID_FORM = 'INVALID_FORM';
|
||||
export const VALID_FORM = 'VALID_FORM';
|
||||
|
||||
export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
|
||||
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
|
||||
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
|
||||
|
||||
export const VERIFY_EMAIL_REQUEST = 'VERIFY_EMAIL_REQUEST';
|
||||
export const VERIFY_EMAIL_SUCCESS = 'VERIFY_EMAIL_SUCCESS';
|
||||
export const VERIFY_EMAIL_FAILURE = 'VERIFY_EMAIL_FAILURE';
|
||||
export const UPDATE_USERNAME = 'UPDATE_USERNAME';
|
||||
|
||||
// Login Popup actions.
|
||||
export const SET_REQUIRE_EMAIL_VERIFICATION = 'SET_REQUIRE_EMAIL_VERIFICATION';
|
||||
export const SET_REDIRECT_URI = 'SET_REDIRECT_URI';
|
||||
|
||||
export const RESET_SIGNIN_DIALOG = 'RESET_SIGNIN_DIALOG';
|
||||
export const UPDATE_STATUS = 'UPDATE_STATUS';
|
||||
@@ -0,0 +1,4 @@
|
||||
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
|
||||
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
|
||||
export const FOCUS_SIGNIN_DIALOG = 'FOCUS_SIGNIN_DIALOG';
|
||||
export const BLUR_SIGNIN_DIALOG = 'BLUR_SIGNIN_DIALOG';
|
||||
@@ -1,14 +1,27 @@
|
||||
import defaultTo from 'lodash/defaultTo';
|
||||
|
||||
const prefix = 'TALK_EMBED_STREAM';
|
||||
|
||||
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
|
||||
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
|
||||
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
|
||||
export const VIEW_COMMENT = 'VIEW_COMMENT';
|
||||
export const ADDTL_COMMENTS_ON_LOAD_MORE = parseInt(
|
||||
defaultTo(process.env.TALK_ADDTL_COMMENTS_ON_LOAD_MORE, '10')
|
||||
);
|
||||
export const ASSET_COMMENTS_LOAD_DEPTH = parseInt(
|
||||
defaultTo(process.env.TALK_ASSET_COMMENTS_LOAD_DEPTH, '10')
|
||||
);
|
||||
export const REPLY_COMMENTS_LOAD_DEPTH = parseInt(
|
||||
defaultTo(process.env.TALK_REPLY_COMMENTS_LOAD_DEPTH, '3')
|
||||
);
|
||||
export const THREADING_LEVEL = parseInt(
|
||||
defaultTo(process.env.TALK_THREADING_LEVEL, '3')
|
||||
);
|
||||
|
||||
export const ADD_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_ADD_TAG`;
|
||||
export const ADD_COMMENT_CLASSNAME = 'ADD_COMMENT_CLASSNAME';
|
||||
export const CLEAR_COMMENT_BOX_TAGS = `${prefix}_COMMENT_BOX_CLEAR_TAGS`;
|
||||
export const REMOVE_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_REMOVE_TAG`;
|
||||
export const REMOVE_COMMENT_CLASSNAME = 'REMOVE_COMMENT_CLASSNAME';
|
||||
export const THREADING_LEVEL = process.env.TALK_THREADING_LEVEL;
|
||||
export const SET_ACTIVE_REPLY_BOX = 'SET_ACTIVE_REPLY_BOX';
|
||||
export const SET_ACTIVE_TAB = 'CORAL_STREAM_SET_ACTIVE_TAB';
|
||||
export const SET_SORT = 'CORAL_STREAM_SET_SORT';
|
||||
export const ADD_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_ADD_TAG`;
|
||||
export const REMOVE_COMMENT_BOX_TAG = `${prefix}_COMMENT_BOX_REMOVE_TAG`;
|
||||
export const CLEAR_COMMENT_BOX_TAGS = `${prefix}_COMMENT_BOX_CLEAR_TAGS`;
|
||||
export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
|
||||
export const VIEW_COMMENT = 'VIEW_COMMENT';
|
||||
|
||||
@@ -8,8 +8,13 @@ import branch from 'recompose/branch';
|
||||
import renderComponent from 'recompose/renderComponent';
|
||||
|
||||
import { Spinner } from 'coral-ui';
|
||||
import * as authActions from '../actions/auth';
|
||||
import * as assetActions from '../actions/asset';
|
||||
import {
|
||||
focusSignInDialog,
|
||||
blurSignInDialog,
|
||||
hideSignInDialog,
|
||||
} from '../actions/login';
|
||||
import { updateStatus } from 'coral-framework/actions/auth';
|
||||
import { fetchAssetSuccess } from '../actions/asset';
|
||||
import {
|
||||
getDefinitionName,
|
||||
getSlotFragmentSpreads,
|
||||
@@ -24,16 +29,6 @@ import t from 'coral-framework/services/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { setActiveTab } from '../actions/embed';
|
||||
|
||||
const {
|
||||
logout,
|
||||
checkLogin,
|
||||
focusSignInDialog,
|
||||
blurSignInDialog,
|
||||
hideSignInDialog,
|
||||
updateStatus,
|
||||
} = authActions;
|
||||
const { fetchAssetSuccess } = assetActions;
|
||||
|
||||
class EmbedContainer extends React.Component {
|
||||
static contextTypes = {
|
||||
pym: PropTypes.object,
|
||||
@@ -42,7 +37,7 @@ class EmbedContainer extends React.Component {
|
||||
subscriptions = [];
|
||||
|
||||
subscribeToUpdates(props = this.props) {
|
||||
if (props.auth.loggedIn) {
|
||||
if (props.currentUser) {
|
||||
const newSubscriptions = [
|
||||
{
|
||||
document: USER_BANNED_SUBSCRIPTION,
|
||||
@@ -80,7 +75,7 @@ class EmbedContainer extends React.Component {
|
||||
props.data.subscribeToMore({
|
||||
document: s.document,
|
||||
variables: {
|
||||
user_id: props.auth.user.id,
|
||||
user_id: props.currentUser.id,
|
||||
},
|
||||
updateQuery: s.updateQuery,
|
||||
})
|
||||
@@ -107,7 +102,7 @@ class EmbedContainer extends React.Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.auth.loggedIn !== nextProps.auth.loggedIn) {
|
||||
if (this.props.currentUser !== nextProps.currentUser) {
|
||||
// Refetch after login/logout.
|
||||
this.props.data.refetch();
|
||||
this.resubscribe(nextProps);
|
||||
@@ -138,7 +133,23 @@ class EmbedContainer extends React.Component {
|
||||
if (!this.props.root.asset) {
|
||||
return <Spinner />;
|
||||
}
|
||||
return <Embed {...this.props} />;
|
||||
return (
|
||||
<Embed
|
||||
setActiveTab={this.props.setActiveTab}
|
||||
currentUser={this.props.currentUser}
|
||||
blurSignInDialog={this.props.blurSignInDialog}
|
||||
focusSignInDialog={this.props.focusSignInDialog}
|
||||
hideSignInDialog={this.props.hideSignInDialog}
|
||||
router={this.props.router}
|
||||
commentId={this.props.commentId}
|
||||
root={this.props.root}
|
||||
activeTab={this.props.activeTab}
|
||||
data={this.props.data}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
signInDialogFocus={this.props.signInDialogFocus}
|
||||
parentUrl={this.props.parentUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,21 +266,46 @@ const EMBED_QUERY = gql`
|
||||
`;
|
||||
|
||||
export const withEmbedQuery = withQuery(EMBED_QUERY, {
|
||||
options: ({ auth, commentId, assetId, assetUrl, sortBy, sortOrder }) => ({
|
||||
options: ({
|
||||
currentUser,
|
||||
commentId,
|
||||
assetId,
|
||||
assetUrl,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
}) => ({
|
||||
variables: {
|
||||
assetId,
|
||||
assetUrl,
|
||||
commentId,
|
||||
hasComment: commentId !== '',
|
||||
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
|
||||
excludeIgnored: Boolean(currentUser && currentUser.id),
|
||||
sortBy,
|
||||
sortOrder,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
EmbedContainer.propTypes = {
|
||||
setActiveTab: PropTypes.func,
|
||||
currentUser: PropTypes.object,
|
||||
blurSignInDialog: PropTypes.func,
|
||||
focusSignInDialog: PropTypes.func,
|
||||
hideSignInDialog: PropTypes.func,
|
||||
router: PropTypes.object,
|
||||
commentId: PropTypes.string,
|
||||
root: PropTypes.object,
|
||||
activeTab: PropTypes.string,
|
||||
parentUrl: PropTypes.string,
|
||||
data: PropTypes.object,
|
||||
fetchAssetSuccess: PropTypes.func,
|
||||
showSignInDialog: PropTypes.bool,
|
||||
signInDialogFocus: PropTypes.bool,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth,
|
||||
currentUser: state.auth.user,
|
||||
checkedInitialLogin: state.auth.checkedInitialLogin,
|
||||
commentId: state.stream.commentId,
|
||||
assetId: state.stream.assetId,
|
||||
assetUrl: state.stream.assetUrl,
|
||||
@@ -277,13 +313,14 @@ const mapStateToProps = state => ({
|
||||
config: state.config,
|
||||
sortOrder: state.stream.sortOrder,
|
||||
sortBy: state.stream.sortBy,
|
||||
showSignInDialog: state.login.showSignInDialog,
|
||||
signInDialogFocus: state.login.signInDialogFocus,
|
||||
parentUrl: state.login.parentUrl,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
logout,
|
||||
checkLogin,
|
||||
setActiveTab,
|
||||
fetchAssetSuccess,
|
||||
notify,
|
||||
@@ -297,6 +334,6 @@ const mapDispatchToProps = dispatch =>
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
branch(props => !props.auth.checkedInitialLogin, renderComponent(Spinner)),
|
||||
branch(props => !props.checkedInitialLogin, renderComponent(Spinner)),
|
||||
withEmbedQuery
|
||||
)(EmbedContainer);
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import React from 'react';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
export const LoginContainer = () => <Slot fill="login" />;
|
||||
@@ -1,67 +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';
|
||||
|
||||
function inIframe() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move init code into `bootstrap` service after auth has been refactored.
|
||||
function preInit({ store, pym }) {
|
||||
// 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),
|
||||
|
||||
@@ -2,13 +2,13 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
// TODO: (kiwi) Need to adapt CSS classes post refactor to match the rest.
|
||||
import { name } from '../containers/CommentBox';
|
||||
import styles from './CommentForm.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import DraftArea from '../containers/DraftArea';
|
||||
|
||||
/**
|
||||
* Common UI for Creating or Editing a Comment
|
||||
@@ -61,10 +61,6 @@ export class CommentForm extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
onBodyChange = e => {
|
||||
this.props.onBodyChange(e.target.value);
|
||||
};
|
||||
|
||||
onClickSubmit = () => {
|
||||
this.props.onSubmit();
|
||||
};
|
||||
@@ -107,37 +103,16 @@ export class CommentForm extends React.Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`${name}-container`}>
|
||||
<label
|
||||
htmlFor={this.props.bodyInputId}
|
||||
className="screen-reader-text"
|
||||
aria-hidden={true}
|
||||
>
|
||||
{this.props.bodyLabel}
|
||||
</label>
|
||||
<textarea
|
||||
className={`${name}-textarea`}
|
||||
value={body}
|
||||
placeholder={this.props.bodyPlaceholder}
|
||||
id={this.props.bodyInputId}
|
||||
onChange={this.onBodyChange}
|
||||
rows={3}
|
||||
disabled={disableTextArea}
|
||||
/>
|
||||
<Slot fill="commentInputArea" />
|
||||
</div>
|
||||
{this.props.charCountEnable && (
|
||||
<div
|
||||
className={`${name}-char-count ${
|
||||
length > maxCharCount ? `${name}-char-max` : ''
|
||||
}`}
|
||||
>
|
||||
{maxCharCount &&
|
||||
`${maxCharCount - length} ${t(
|
||||
'comment_box.characters_remaining'
|
||||
)}`}
|
||||
</div>
|
||||
)}
|
||||
<DraftArea
|
||||
id={this.props.bodyInputId}
|
||||
label={this.props.bodyLabel}
|
||||
value={body}
|
||||
placeholder={this.props.bodyPlaceholder}
|
||||
onChange={this.props.onBodyChange}
|
||||
disabled={disableTextArea}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
maxCharCount={this.props.maxCharCount}
|
||||
/>
|
||||
<div className={`${name}-button-container`}>
|
||||
{this.props.buttonContainerStart}
|
||||
{typeof this.props.onCancel === 'function' && (
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
// TODO: (kiwi) Need to adapt CSS classes post refactor to match the rest.
|
||||
|
||||
/**
|
||||
* An enhanced textarea to make comment drafts.
|
||||
*/
|
||||
export default class DraftArea extends React.Component {
|
||||
renderCharCount() {
|
||||
const { value, maxCharCount } = this.props;
|
||||
const className = cn('talk-plugin-commentbox-char-count', {
|
||||
['talk-plugin-commentbox-char-max']: value.length > maxCharCount,
|
||||
});
|
||||
const remaining = maxCharCount - value.length;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{remaining} {t('comment_box.characters_remaining')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
placeholder,
|
||||
id,
|
||||
disabled,
|
||||
rows,
|
||||
label,
|
||||
charCountEnable,
|
||||
maxCharCount,
|
||||
onChange,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={'talk-plugin-commentbox-container'}>
|
||||
<label htmlFor={id} className="screen-reader-text" aria-hidden={true}>
|
||||
{label}
|
||||
</label>
|
||||
<textarea
|
||||
className={'talk-plugin-commentbox-textarea'}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
id={id}
|
||||
onChange={onChange}
|
||||
rows={rows}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Slot fill="commentInputArea" />
|
||||
</div>
|
||||
{charCountEnable && maxCharCount > 0 && this.renderCharCount()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DraftArea.defaultProps = {
|
||||
rows: 3,
|
||||
};
|
||||
|
||||
DraftArea.propTypes = {
|
||||
charCountEnable: PropTypes.bool,
|
||||
maxCharCount: PropTypes.number,
|
||||
id: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
rows: PropTypes.number,
|
||||
};
|
||||
@@ -21,6 +21,7 @@ export class EditableCommentContent extends React.Component {
|
||||
|
||||
// comment that is being edited
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
editing: PropTypes.shape({
|
||||
edited: PropTypes.bool,
|
||||
@@ -120,10 +121,12 @@ export class EditableCommentContent extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const id = `edit-draft_${this.props.comment.id}`;
|
||||
return (
|
||||
<div className={styles.editCommentForm}>
|
||||
<CommentForm
|
||||
defaultValue={this.props.comment.body}
|
||||
bodyInputId={id}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
maxCharCount={this.props.maxCharCount}
|
||||
submitEnabled={this.isSubmitEnabled}
|
||||
|
||||
@@ -7,7 +7,8 @@ const name = 'talk-plugin-replies';
|
||||
|
||||
class ReplyBox extends Component {
|
||||
componentDidMount() {
|
||||
document.getElementById('replyText').focus();
|
||||
// TODO: (kiwi) This does not follow best practices, better to move this logic into the component.
|
||||
document.getElementById(`comment-draft_${this.props.parentId}`).focus();
|
||||
}
|
||||
|
||||
cancelReply = () => {
|
||||
@@ -54,6 +55,8 @@ ReplyBox.propTypes = {
|
||||
notify: PropTypes.func.isRequired,
|
||||
postComment: PropTypes.func.isRequired,
|
||||
assetId: PropTypes.string.isRequired,
|
||||
currentUser: PropTypes.object,
|
||||
styles: PropTypes.object,
|
||||
};
|
||||
|
||||
export default ReplyBox;
|
||||
|
||||
@@ -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,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import { isSuspended } from 'coral-framework/utils/user';
|
||||
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -20,7 +21,6 @@ class CommentBox extends React.Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
username: '',
|
||||
body: '',
|
||||
loadingState: '',
|
||||
|
||||
@@ -41,6 +41,17 @@ class CommentBox extends React.Component {
|
||||
currentUser,
|
||||
} = this.props;
|
||||
|
||||
if (isSuspended(currentUser)) {
|
||||
notify(
|
||||
'error',
|
||||
t(
|
||||
'error.temporarily_suspended',
|
||||
timeago(currentUser.status.suspension.until)
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!can(currentUser, 'INTERACT_WITH_COMMUNITY')) {
|
||||
notify('error', t('error.NOT_AUTHORIZED'));
|
||||
return;
|
||||
@@ -129,7 +140,7 @@ class CommentBox extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isReply, maxCharCount } = this.props;
|
||||
const { isReply, maxCharCount, assetId, parentId } = this.props;
|
||||
let { onCancel } = this.props;
|
||||
|
||||
if (isReply && typeof onCancel !== 'function') {
|
||||
@@ -139,6 +150,11 @@ class CommentBox extends React.Component {
|
||||
onCancel = () => {};
|
||||
}
|
||||
|
||||
// Generate id for the DraftArea.
|
||||
const id = parentId
|
||||
? `comment-draft_${parentId}`
|
||||
: `comment-draft_${assetId}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CommentForm
|
||||
@@ -147,7 +163,7 @@ class CommentBox extends React.Component {
|
||||
maxCharCount={maxCharCount}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
bodyPlaceholder={t('comment.comment')}
|
||||
bodyInputId={isReply ? 'replyText' : 'commentText'}
|
||||
bodyInputId={id}
|
||||
body={this.state.body}
|
||||
buttonContainerStart={
|
||||
<Slot
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DraftArea from '../components/DraftArea';
|
||||
|
||||
const STORAGE_PATH = 'DraftArea';
|
||||
|
||||
/**
|
||||
* An enhanced textarea to make comment drafts.
|
||||
*/
|
||||
export default class DraftAreaContainer extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.initValue();
|
||||
}
|
||||
|
||||
async initValue() {
|
||||
const value = await this.context.pymSessionStorage.getItem(this.getPath());
|
||||
if (value && this.props.onChange) {
|
||||
this.props.onChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
getPath = () => {
|
||||
return `${STORAGE_PATH}_${this.props.id}`;
|
||||
};
|
||||
|
||||
onChange = e => {
|
||||
this.props.onChange && this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.value !== nextProps.value) {
|
||||
if (nextProps.value) {
|
||||
this.context.pymSessionStorage.setItem(this.getPath(), nextProps.value);
|
||||
} else {
|
||||
this.context.pymSessionStorage.removeItem(this.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DraftArea
|
||||
value={this.props.value}
|
||||
placeholder={this.props.placeholder}
|
||||
id={this.props.id}
|
||||
onChange={this.onChange}
|
||||
rows={this.props.rows}
|
||||
disabled={this.props.disabled}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
maxCharCount={this.props.maxCharCount}
|
||||
label={this.props.label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DraftAreaContainer.contextTypes = {
|
||||
// We use pymSessionStorage instead to persist the data directly on the parent page,
|
||||
// in order to mitigate strict cross domain security settings.
|
||||
pymSessionStorage: PropTypes.object,
|
||||
};
|
||||
|
||||
DraftAreaContainer.propTypes = {
|
||||
charCountEnable: PropTypes.bool,
|
||||
maxCharCount: PropTypes.number,
|
||||
id: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
rows: PropTypes.number,
|
||||
label: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -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 */
|
||||
|
||||
|
||||
|
||||
@@ -146,8 +146,19 @@ export default class Stream {
|
||||
// If the user clicks outside the embed, then tell the embed.
|
||||
document.addEventListener('click', this.handleClick.bind(this), true);
|
||||
|
||||
// Listens to storage requests on pym and relay it to local storage.
|
||||
connectStorageToPym(createStorage(), this.pym);
|
||||
// Listens to local storage requests on pym and relay it to local storage.
|
||||
connectStorageToPym(
|
||||
createStorage('localStorage'),
|
||||
this.pym,
|
||||
'localStorage'
|
||||
);
|
||||
|
||||
// Listens to session storage requests on pym and relay it to session storage.
|
||||
connectStorageToPym(
|
||||
createStorage('sessionStorage'),
|
||||
this.pym,
|
||||
'sessionStorage'
|
||||
);
|
||||
}
|
||||
|
||||
login(token) {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import * as actions from '../constants/auth';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
function cleanAuthData(localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
|
||||
export const checkLogin = () => (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, pym, localStorage }
|
||||
) => {
|
||||
dispatch(checkLoginRequest());
|
||||
rest('/auth')
|
||||
.then(result => {
|
||||
if (!result.user) {
|
||||
if (localStorage) {
|
||||
cleanAuthData(localStorage);
|
||||
}
|
||||
dispatch(checkLoginSuccess(null));
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch(checkLoginSuccess(result.user));
|
||||
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status && error.status === 401 && localStorage) {
|
||||
// Unauthorized.
|
||||
cleanAuthData(localStorage);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
dispatch(checkLoginFailure(error));
|
||||
});
|
||||
};
|
||||
|
||||
const checkLoginRequest = () => ({ type: actions.CHECK_LOGIN_REQUEST });
|
||||
|
||||
const checkLoginFailure = error => ({
|
||||
type: actions.CHECK_LOGIN_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
const checkLoginSuccess = user => ({
|
||||
type: actions.CHECK_LOGIN_SUCCESS,
|
||||
user,
|
||||
});
|
||||
|
||||
export const setAuthToken = token => (dispatch, _, { localStorage }) => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
dispatch(checkLogin());
|
||||
};
|
||||
|
||||
export const handleSuccessfulLogin = (user, token) => (
|
||||
dispatch,
|
||||
_,
|
||||
{ client, localStorage }
|
||||
) => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({
|
||||
type: actions.HANDLE_SUCCESSFUL_LOGIN,
|
||||
user,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
export const logout = () => async (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, pym, localStorage }
|
||||
) => {
|
||||
await rest('/auth', { method: 'DELETE' });
|
||||
|
||||
if (localStorage) {
|
||||
cleanAuthData(localStorage);
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({ type: actions.LOGOUT });
|
||||
pym.sendMessage('coral-auth-changed');
|
||||
};
|
||||
|
||||
export const updateStatus = status => ({
|
||||
type: actions.UPDATE_STATUS,
|
||||
status,
|
||||
});
|
||||
|
||||
export const updateUsername = username => ({
|
||||
type: actions.UPDATE_USERNAME,
|
||||
username,
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MERGE_CONFIG } from '../constants/config';
|
||||
|
||||
export const mergeConfig = config => ({
|
||||
type: MERGE_CONFIG,
|
||||
config,
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactRecaptcha from 'react-recaptcha';
|
||||
|
||||
class Recaptcha extends React.Component {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
};
|
||||
|
||||
ref = null;
|
||||
|
||||
handleRef = ref => {
|
||||
this.ref = ref;
|
||||
};
|
||||
|
||||
reset = () => this.ref.reset();
|
||||
|
||||
getSiteKey() {
|
||||
// This should be fine because it's static and will never change.
|
||||
// Prefer this to connect HOC because wie expose the instance method
|
||||
// `reset`
|
||||
return this.context.store.getState().config.static.TALK_RECAPTCHA_PUBLIC;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ReactRecaptcha
|
||||
ref={this.handleRef}
|
||||
sitekey={this.getSiteKey()}
|
||||
render={this.props.render}
|
||||
theme={this.props.theme}
|
||||
onloadCallback={this.props.onLoad}
|
||||
verifyCallback={this.props.onVerify}
|
||||
size={this.props.size}
|
||||
className={this.props.className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Recaptcha.defaultProps = {
|
||||
render: 'explicit',
|
||||
theme: 'light',
|
||||
size: 'normal',
|
||||
};
|
||||
|
||||
Recaptcha.propTypes = {
|
||||
onLoad: PropTypes.func,
|
||||
onVerify: PropTypes.func.isRequired,
|
||||
theme: PropTypes.string,
|
||||
render: PropTypes.string,
|
||||
size: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Recaptcha;
|
||||
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import get from 'lodash/get';
|
||||
import { getShallowChanges } from 'coral-framework/utils';
|
||||
|
||||
const emptyConfig = {};
|
||||
@@ -68,7 +69,7 @@ class Slot extends React.Component {
|
||||
} = this.props;
|
||||
const { plugins } = this.context;
|
||||
let children = this.getChildren();
|
||||
const pluginConfig = reduxState.config.pluginConfig || emptyConfig;
|
||||
const pluginConfig = get(reduxState, 'config.pluginConfig') || emptyConfig;
|
||||
if (children.length === 0 && DefaultComponent) {
|
||||
const props = plugins.getSlotComponentProps(
|
||||
DefaultComponent,
|
||||
|
||||
@@ -11,9 +11,12 @@ class TalkProvider extends React.Component {
|
||||
rest: this.props.rest,
|
||||
graphql: this.props.graphql,
|
||||
notification: this.props.notification,
|
||||
storage: this.props.storage,
|
||||
localStorage: this.props.localStorage,
|
||||
sessionStorage: this.props.sessionStorage,
|
||||
history: this.props.history,
|
||||
store: this.props.store,
|
||||
pymLocalStorage: this.props.pymLocalStorage,
|
||||
pymSessionStorage: this.props.pymSessionStorage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,9 +33,14 @@ TalkProvider.childContextTypes = {
|
||||
rest: PropTypes.func,
|
||||
graphql: PropTypes.object,
|
||||
notification: PropTypes.object,
|
||||
storage: PropTypes.object,
|
||||
localStorage: PropTypes.object,
|
||||
sessionStorage: PropTypes.object,
|
||||
pymLocalStorage: PropTypes.object,
|
||||
pymSessionStorage: PropTypes.object,
|
||||
history: PropTypes.object,
|
||||
store: PropTypes.object,
|
||||
};
|
||||
|
||||
TalkProvider.propTypes = TalkProvider.childContextTypes;
|
||||
|
||||
export default TalkProvider;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const prefix = `TALK_FRAMEWORK`;
|
||||
|
||||
export const CHECK_LOGIN_REQUEST = `${prefix}_CHECK_LOGIN_REQUEST`;
|
||||
export const CHECK_LOGIN_SUCCESS = `${prefix}_CHECK_LOGIN_SUCCESS`;
|
||||
export const CHECK_LOGIN_FAILURE = `${prefix}_CHECK_LOGIN_FAILURE`;
|
||||
|
||||
export const LOGOUT = `${prefix}_LOGOUT`;
|
||||
export const HANDLE_SUCCESSFUL_LOGIN = `${prefix}_HANDLE_SUCCESSFUL_LOGIN`;
|
||||
|
||||
export const UPDATE_STATUS = '${prefix}_UPDATE_STATUS';
|
||||
export const UPDATE_USERNAME = '${prefix}_UPDATE_USERNAME';
|
||||
@@ -0,0 +1,3 @@
|
||||
const prefix = `TALK_FRAMEWORK`;
|
||||
|
||||
export const MERGE_CONFIG = `${prefix}_MERGE_CONFIG`;
|
||||
@@ -6,3 +6,10 @@ export { default as withEmit } from './withEmit';
|
||||
export { default as excludeIf } from './excludeIf';
|
||||
export { default as connect } from './connect';
|
||||
export { default as withMergedSettings } from './withMergedSettings';
|
||||
export { default as withSignIn } from './withSignIn';
|
||||
export { default as withSignUp } from './withSignUp';
|
||||
export { default as withForgotPassword } from './withForgotPassword';
|
||||
export { default as withSetUsername } from './withSetUsername';
|
||||
export {
|
||||
default as withResendEmailConfirmation,
|
||||
} from './withResendEmailConfirmation';
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translateError } from '../utils';
|
||||
|
||||
/**
|
||||
* WithForgotPassword provides properties
|
||||
* `forgotPasssword`,
|
||||
* `loading`,
|
||||
* `errorMessage`,
|
||||
* `success`.
|
||||
*/
|
||||
export default hoistStatics(WrappedComponent => {
|
||||
class WithForgotPassword extends React.Component {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
rest: PropTypes.func,
|
||||
pym: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
success: false,
|
||||
};
|
||||
|
||||
forgotPassword = (email, redirectUri) => {
|
||||
if (!redirectUri) {
|
||||
redirectUri = this.context.pym.parentUrl || location.href;
|
||||
}
|
||||
const { rest } = this.context;
|
||||
this.setState({ loading: true, error: null, success: false });
|
||||
|
||||
rest('/account/password/reset', {
|
||||
method: 'POST',
|
||||
body: { email, loc: redirectUri },
|
||||
})
|
||||
.then(() => {
|
||||
this.setState({ loading: false, error: null, success: true });
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
this.setState({ loading: false, error });
|
||||
});
|
||||
};
|
||||
|
||||
getErrorMessage() {
|
||||
if (!this.state.error) {
|
||||
return null;
|
||||
}
|
||||
return translateError(this.state.error);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
forgotPassword={this.forgotPassword}
|
||||
success={this.state.success}
|
||||
loading={this.state.loading}
|
||||
errorMessage={this.getErrorMessage()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithForgotPassword;
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translateError } from '../utils';
|
||||
|
||||
/**
|
||||
* WithResendEmailConfirmaton provides properties
|
||||
* `resendEmailConfirmation`,
|
||||
* `loading`,
|
||||
* `errorMessage`,
|
||||
* `success`.
|
||||
*/
|
||||
export default hoistStatics(WrappedComponent => {
|
||||
class WithResendEmailConfirmaton extends React.Component {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
rest: PropTypes.func,
|
||||
pym: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
success: false,
|
||||
};
|
||||
|
||||
resendEmailConfirmation = (email, redirectUri) => {
|
||||
if (!redirectUri) {
|
||||
redirectUri = this.context.pym.parentUrl || location.href;
|
||||
}
|
||||
const { rest } = this.context;
|
||||
this.setState({ loading: true, error: null, success: false });
|
||||
|
||||
rest('/users/resend-verify', {
|
||||
method: 'POST',
|
||||
body: { email },
|
||||
headers: { 'X-Pym-Url': redirectUri },
|
||||
})
|
||||
.then(() => {
|
||||
this.setState({ loading: false, error: null, success: true });
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
this.setState({ loading: false, error });
|
||||
});
|
||||
};
|
||||
|
||||
getErrorMessage() {
|
||||
if (!this.state.error) {
|
||||
return null;
|
||||
}
|
||||
return translateError(this.state.error);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
resendEmailConfirmation={this.resendEmailConfirmation}
|
||||
success={this.state.success}
|
||||
loading={this.state.loading}
|
||||
errorMessage={this.getErrorMessage()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithResendEmailConfirmaton;
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getErrorMessages } from '../utils';
|
||||
import validate from '../helpers/validate';
|
||||
import errorMsg from 'coral-framework/helpers/error';
|
||||
import t from '../services/i18n';
|
||||
import { withSetUsername as withSetUsernameMutation } from 'coral-framework/graphql/mutations';
|
||||
import { updateUsername, updateStatus } from '../actions/auth';
|
||||
import { compose } from 'recompose';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import get from 'lodash/get';
|
||||
|
||||
/**
|
||||
* withSetUsername provides properties
|
||||
* `setUsername`,
|
||||
* `loading`,
|
||||
* `errorMessage`,
|
||||
* `requireEmailVerification`,
|
||||
* `success`,
|
||||
* `validateUsername`.
|
||||
*/
|
||||
const withSetUsername = hoistStatics(WrappedComponent => {
|
||||
class WithSetUsername extends React.Component {
|
||||
static propTypes = {
|
||||
setUsername: PropTypes.func.isRequired,
|
||||
currentUserId: PropTypes.string,
|
||||
updateUsername: PropTypes.func.isRequired,
|
||||
updateStatus: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
success: false,
|
||||
};
|
||||
|
||||
validateUsername = value => {
|
||||
if (!value) {
|
||||
return t('error.required_field');
|
||||
}
|
||||
return validate.username(value) ? null : errorMsg.username;
|
||||
};
|
||||
|
||||
setUsername = async username => {
|
||||
if (!this.props.currentUserId) {
|
||||
throw new Error('User not logged in');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.props.setUsername(this.props.currentUserId, username);
|
||||
this.props.updateUsername(username);
|
||||
this.props.updateStatus({ username: { status: 'SET' } });
|
||||
this.setState({ success: true, loading: false, error: null });
|
||||
} catch (error) {
|
||||
if (!error.status || error.status !== 401) {
|
||||
console.error(error);
|
||||
}
|
||||
const changeSet = { success: false, loading: false, error };
|
||||
this.setState(changeSet);
|
||||
}
|
||||
};
|
||||
|
||||
getErrorMessage() {
|
||||
if (!this.state.error) {
|
||||
return null;
|
||||
}
|
||||
return getErrorMessages(this.state.error).join(', ');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
setUsername={this.setUsername}
|
||||
loading={this.state.loading}
|
||||
errorMessage={this.getErrorMessage()}
|
||||
success={this.state.success}
|
||||
validateUsername={this.validateUsername}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithSetUsername;
|
||||
});
|
||||
|
||||
const mapStateToProps = ({ auth }) => ({
|
||||
currentUserId: get(auth, 'user.id'),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ updateUsername, updateStatus }, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetUsernameMutation,
|
||||
withSetUsername
|
||||
);
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import PropTypes from 'prop-types';
|
||||
import { handleSuccessfulLogin } from '../actions/auth';
|
||||
import { translateError } from '../utils';
|
||||
import { t } from '../services/i18n';
|
||||
|
||||
/**
|
||||
* WithSignIn provides properties
|
||||
* `signIn`
|
||||
* `loading`
|
||||
* `errorMessage`
|
||||
* `requireRecaptcha`
|
||||
* `requireEmailConfirmation`
|
||||
* 'success'
|
||||
*/
|
||||
export default hoistStatics(WrappedComponent => {
|
||||
class WithSignIn extends React.Component {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
rest: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
success: false,
|
||||
requireRecaptcha: false,
|
||||
requireEmailConfirmation: false,
|
||||
};
|
||||
|
||||
signIn = (email, password, recaptchaResponse) => {
|
||||
const { store, rest } = this.context;
|
||||
const params = {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
};
|
||||
|
||||
if (recaptchaResponse) {
|
||||
params.headers = {
|
||||
'X-Recaptcha-Response': recaptchaResponse,
|
||||
};
|
||||
}
|
||||
|
||||
rest('/auth/local', params)
|
||||
.then(({ user, token }) => {
|
||||
this.setState({ success: true, loading: false, error: null });
|
||||
store.dispatch(handleSuccessfulLogin(user, token));
|
||||
})
|
||||
.catch(error => {
|
||||
if (!error.status || error.status !== 401) {
|
||||
console.error(error);
|
||||
}
|
||||
const changeSet = { success: false, loading: false, error };
|
||||
if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
changeSet.requireRecaptcha = !!this.context.store.getState().config
|
||||
.static.TALK_RECAPTCHA_PUBLIC;
|
||||
} else if (error.translation_key === 'EMAIL_NOT_VERIFIED') {
|
||||
changeSet.requireEmailConfirmation = true;
|
||||
}
|
||||
this.setState(changeSet);
|
||||
});
|
||||
};
|
||||
|
||||
getErrorMessage() {
|
||||
if (!this.state.error) {
|
||||
return null;
|
||||
}
|
||||
return this.state.error.translation_key === 'NOT_AUTHORIZED'
|
||||
? t('error.email_password')
|
||||
: translateError(this.state.error);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
signIn={this.signIn}
|
||||
loading={this.state.loading}
|
||||
errorMessage={this.getErrorMessage()}
|
||||
requireRecaptcha={this.state.requireRecaptcha}
|
||||
requireEmailConfirmation={this.state.requireEmailConfirmation}
|
||||
success={this.state.success}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithSignIn;
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translateError } from '../utils';
|
||||
import validate from '../helpers/validate';
|
||||
import errorMsg from 'coral-framework/helpers/error';
|
||||
import t from '../services/i18n';
|
||||
import withQuery from './withQuery';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const requiredFields = ['username', 'email', 'password'];
|
||||
const allFields = requiredFields;
|
||||
|
||||
const QUERY = gql`
|
||||
query TalkFramework_WithSignUpQuery {
|
||||
settings {
|
||||
requireEmailConfirmation
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const withSettingsQuery = withQuery(QUERY);
|
||||
|
||||
/**
|
||||
* withSignUp provides properties
|
||||
* `signUp`,
|
||||
* `loading`,
|
||||
* `errorMessage`,
|
||||
* `requireEmailVerification`,
|
||||
* `success`,
|
||||
* `validate`.
|
||||
*/
|
||||
const withSignUp = hoistStatics(WrappedComponent => {
|
||||
class WithSignUp extends React.Component {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
rest: PropTypes.func,
|
||||
pym: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
root: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
success: false,
|
||||
};
|
||||
|
||||
validate = (field, value) => {
|
||||
if (!allFields.includes(field)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (requiredFields.includes(field) && !value) {
|
||||
return t('error.required_field');
|
||||
}
|
||||
|
||||
if (field in validate) {
|
||||
return validate[field](value) ? null : errorMsg[field];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
signUp = ({ username, email, password }, redirectUri) => {
|
||||
if (!redirectUri) {
|
||||
redirectUri = this.context.pym.parentUrl || location.href;
|
||||
}
|
||||
|
||||
const { rest } = this.context;
|
||||
const params = {
|
||||
method: 'POST',
|
||||
body: {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
},
|
||||
headers: { 'X-Pym-Url': redirectUri },
|
||||
};
|
||||
|
||||
rest('/users', params)
|
||||
.then(() => {
|
||||
this.setState({ success: true, loading: false, error: null });
|
||||
})
|
||||
.catch(error => {
|
||||
if (!error.status || error.status !== 401) {
|
||||
console.error(error);
|
||||
}
|
||||
const changeSet = { success: false, loading: false, error };
|
||||
this.setState(changeSet);
|
||||
});
|
||||
};
|
||||
|
||||
getErrorMessage() {
|
||||
if (!this.state.error) {
|
||||
return null;
|
||||
}
|
||||
return translateError(this.state.error);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
signUp={this.signUp}
|
||||
loading={this.state.loading}
|
||||
errorMessage={this.getErrorMessage()}
|
||||
requireEmailConfirmation={
|
||||
!!get(this.props, 'root.settings.requireEmailConfirmation')
|
||||
}
|
||||
success={this.state.success}
|
||||
validate={this.validate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithSignUp;
|
||||
});
|
||||
|
||||
export default compose(withSettingsQuery, withSignUp);
|
||||
@@ -0,0 +1,61 @@
|
||||
import * as actions from '../constants/auth';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
const initialState = {
|
||||
checkedInitialLogin: false,
|
||||
initialLoginError: null,
|
||||
user: null,
|
||||
};
|
||||
|
||||
const purge = user => {
|
||||
const {settings, ...userData} = user; // eslint-disable-line
|
||||
return userData;
|
||||
};
|
||||
|
||||
export default function auth(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
initialLoginError: action.error,
|
||||
checkedInitialLogin: true,
|
||||
user: null,
|
||||
};
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
checkedInitialLogin: true,
|
||||
user: action.user ? purge(action.user) : null,
|
||||
};
|
||||
case actions.HANDLE_SUCCESSFUL_LOGIN:
|
||||
return {
|
||||
...state,
|
||||
user: action.user ? purge(action.user) : null,
|
||||
};
|
||||
case actions.LOGOUT:
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
};
|
||||
case actions.UPDATE_STATUS: {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
status: merge({}, state.user.status, action.status),
|
||||
},
|
||||
};
|
||||
}
|
||||
case actions.UPDATE_USERNAME:
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
username: action.username,
|
||||
lowercaseUsername: action.username.toLowerCase(),
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
+4
-6
@@ -1,15 +1,13 @@
|
||||
import * as actions from '../actions/config';
|
||||
import { MERGE_CONFIG } from '../constants/config';
|
||||
|
||||
const initialState = {
|
||||
data: {},
|
||||
};
|
||||
const initialState = {};
|
||||
|
||||
export default function config(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CONFIG_UPDATED:
|
||||
case MERGE_CONFIG:
|
||||
return {
|
||||
...state,
|
||||
data: action.data,
|
||||
...action.config,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
@@ -0,0 +1,8 @@
|
||||
import auth from './auth';
|
||||
import config from './config';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
login: auth,
|
||||
config,
|
||||
};
|
||||
+78
-9
@@ -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) {
|
||||
@@ -43,6 +47,27 @@ const getAuthToken = (store, storage) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
function areWeInIframe() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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`.
|
||||
@@ -62,10 +87,19 @@ export async function createContext({
|
||||
notification,
|
||||
preInit,
|
||||
init = noop,
|
||||
checkLogin = true,
|
||||
addExternalConfig = true,
|
||||
} = {}) {
|
||||
const inIframe = areWeInIframe();
|
||||
const eventEmitter = new EventEmitter({ wildcard: true });
|
||||
const storage = createStorage();
|
||||
const pymStorage = createPymStorage(pym);
|
||||
const localStorage = createStorage('localStorage');
|
||||
const sessionStorage = createStorage('sessionStorage');
|
||||
const pymLocalStorage = inIframe
|
||||
? createPymStorage(pym, 'localStorage')
|
||||
: localStorage;
|
||||
const pymSessionStorage = inIframe
|
||||
? createPymStorage(pym, 'sessionStorage')
|
||||
: sessionStorage;
|
||||
const history = createHistory(BASE_PATH);
|
||||
const introspection = createIntrospection(introspectionData);
|
||||
let store = null;
|
||||
@@ -75,7 +109,7 @@ export async function createContext({
|
||||
|
||||
// NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT
|
||||
// TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT.
|
||||
return getAuthToken(store, storage);
|
||||
return getAuthToken(store, localStorage);
|
||||
};
|
||||
|
||||
const rest = createRestClient({
|
||||
@@ -83,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';
|
||||
@@ -116,10 +151,13 @@ export async function createContext({
|
||||
rest,
|
||||
graphql,
|
||||
notification,
|
||||
storage,
|
||||
localStorage,
|
||||
sessionStorage,
|
||||
history,
|
||||
introspection,
|
||||
pymStorage,
|
||||
pymLocalStorage,
|
||||
pymSessionStorage,
|
||||
inIframe,
|
||||
};
|
||||
|
||||
// Load framework fragments.
|
||||
@@ -143,6 +181,7 @@ export async function createContext({
|
||||
|
||||
// Create our redux store.
|
||||
const finalReducers = {
|
||||
...coreReducers,
|
||||
...reducers,
|
||||
...plugins.getReducers(),
|
||||
};
|
||||
@@ -162,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.
|
||||
|
||||
@@ -5,6 +5,7 @@ import merge from 'lodash/merge';
|
||||
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/da';
|
||||
import 'moment/locale/de';
|
||||
import 'moment/locale/es';
|
||||
import 'moment/locale/fr';
|
||||
import 'moment/locale/pt-br';
|
||||
@@ -12,6 +13,7 @@ import 'moment/locale/pt-br';
|
||||
import { createStorage } from 'coral-framework/services/storage';
|
||||
|
||||
import daTA from 'timeago.js/locales/da';
|
||||
import deTA from 'timeago.js/locales/de';
|
||||
import esTA from 'timeago.js/locales/es';
|
||||
import frTA from 'timeago.js/locales/fr';
|
||||
import pt_BRTA from 'timeago.js/locales/pt_BR';
|
||||
@@ -21,6 +23,7 @@ import nl from 'timeago.js/locales/nl';
|
||||
|
||||
import en from '../../../locales/en.yml';
|
||||
import da from '../../../locales/da.yml';
|
||||
import de from '../../../locales/de.yml';
|
||||
import es from '../../../locales/es.yml';
|
||||
import fr from '../../../locales/fr.yml';
|
||||
import pt_BR from '../../../locales/pt_BR.yml';
|
||||
@@ -32,6 +35,7 @@ const defaultLanguage = process.env.TALK_DEFAULT_LANG;
|
||||
const translations = {
|
||||
...en,
|
||||
...da,
|
||||
...de,
|
||||
...es,
|
||||
...fr,
|
||||
...nl_NL,
|
||||
@@ -68,7 +72,7 @@ function getLocale(storage) {
|
||||
|
||||
export function setupTranslations() {
|
||||
// Setup the translation framework with the storage.
|
||||
const storage = createStorage();
|
||||
const storage = createStorage('localStorage');
|
||||
|
||||
const locale = getLocale(storage);
|
||||
setLocale(storage, locale);
|
||||
@@ -86,6 +90,7 @@ export function setupTranslations() {
|
||||
|
||||
ta.register('es', esTA);
|
||||
ta.register('da', daTA);
|
||||
ta.register('de', deTA);
|
||||
ta.register('fr', frTA);
|
||||
ta.register('pt_BR', pt_BRTA);
|
||||
ta.register('zh_CN', zh_CNTA);
|
||||
@@ -113,17 +118,22 @@ export function timeago(time) {
|
||||
* any extra parameters are optional and replace a variable marked by {0}, {1}, etc in the translation.
|
||||
*/
|
||||
export function t(key, ...replacements) {
|
||||
const fullKey = `${lang}.${key}`;
|
||||
if (has(translations, fullKey)) {
|
||||
let translation = get(translations, fullKey);
|
||||
let translation;
|
||||
if (has(translations[lang], key)) {
|
||||
translation = get(translations[lang], key);
|
||||
} else if (has(translations['en'], key)) {
|
||||
translation = get(translations['en'], key);
|
||||
console.warn(`${lang}.${key} language key not set`);
|
||||
}
|
||||
|
||||
if (translation) {
|
||||
// replace any {n} with the arguments passed to this method
|
||||
replacements.forEach((str, i) => {
|
||||
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
|
||||
});
|
||||
return translation;
|
||||
} else {
|
||||
console.warn(`${fullKey} language key not set`);
|
||||
console.warn(`${lang}.${key} and en.${key} language key not set`);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -34,8 +34,8 @@ function getStorage(type) {
|
||||
* createStorage returns a localStorage wrapper if available
|
||||
* @return {Object} localStorage wrapper
|
||||
*/
|
||||
export function createStorage() {
|
||||
return getStorage('localStorage');
|
||||
export function createStorage(type = 'localStorage') {
|
||||
return getStorage(type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +44,7 @@ export function createStorage() {
|
||||
* @param {string} pym pym
|
||||
* @return {Object} storage
|
||||
*/
|
||||
export function createPymStorage(pym) {
|
||||
export function createPymStorage(pym, type = 'localStorage') {
|
||||
// A Map of requestID => {resolve, reject}
|
||||
const requests = {};
|
||||
|
||||
@@ -54,21 +54,21 @@ export function createPymStorage(pym) {
|
||||
return new Promise((resolve, reject) => {
|
||||
requests[id] = { resolve, reject };
|
||||
pym.sendMessage(
|
||||
'pymStorage.request',
|
||||
`pymStorage.${type}.request`,
|
||||
JSON.stringify({ id, method, parameters })
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Receive successful responses.
|
||||
pym.onMessage('pymStorage.response', msg => {
|
||||
pym.onMessage(`pymStorage.${type}.response`, msg => {
|
||||
const { id, result } = JSON.parse(msg);
|
||||
requests[id].resolve(result);
|
||||
delete requests[id];
|
||||
});
|
||||
|
||||
// Receive error responses.
|
||||
pym.onMessage('pymStorage.error', msg => {
|
||||
pym.onMessage(`pymStorage.${type}.error`, msg => {
|
||||
const { id, error } = JSON.parse(msg);
|
||||
requests[id].reject(error);
|
||||
delete requests[id];
|
||||
@@ -88,8 +88,13 @@ export function createPymStorage(pym) {
|
||||
* @param {Object} pym pym to listen to storage requests
|
||||
* @param {string} prefix namespace requests by prepending a prefix to the keys
|
||||
*/
|
||||
export function connectStorageToPym(storage, pym, prefix = 'talkPymStorage:') {
|
||||
pym.onMessage('pymStorage.request', msg => {
|
||||
export function connectStorageToPym(
|
||||
storage,
|
||||
pym,
|
||||
type = 'localStorage',
|
||||
prefix = 'talkPymStorage:'
|
||||
) {
|
||||
pym.onMessage(`pymStorage.${type}.request`, msg => {
|
||||
const { id, method, parameters } = JSON.parse(msg);
|
||||
const { key, value } = parameters;
|
||||
const prefixedKey = `${prefix}${key}`;
|
||||
@@ -99,7 +104,10 @@ export function connectStorageToPym(storage, pym, prefix = 'talkPymStorage:') {
|
||||
|
||||
const sendError = error => {
|
||||
console.error(error);
|
||||
pym.sendMessage('pymStorage.error', JSON.stringify({ id, error }));
|
||||
pym.sendMessage(
|
||||
`pymStorage.${type}.error`,
|
||||
JSON.stringify({ id, error })
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -122,6 +130,9 @@ export function connectStorageToPym(storage, pym, prefix = 'talkPymStorage:') {
|
||||
return;
|
||||
}
|
||||
|
||||
pym.sendMessage('pymStorage.response', JSON.stringify({ id, result }));
|
||||
pym.sendMessage(
|
||||
`pymStorage.${type}.response`,
|
||||
JSON.stringify({ id, result })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -252,3 +252,12 @@ export function mapLeaves(o, mapper) {
|
||||
return mapper(val);
|
||||
});
|
||||
}
|
||||
|
||||
export function translateError(error) {
|
||||
if (error.translation_key) {
|
||||
return t(`error.${error.translation_key}`);
|
||||
} else if (error.networkError) {
|
||||
return t('error.network_error');
|
||||
}
|
||||
return error.toString();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
class Main extends React.Component {
|
||||
render() {
|
||||
return <Slot fill="login" />;
|
||||
}
|
||||
}
|
||||
|
||||
export default Main;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
|
||||
import { createContext } from 'coral-framework/services/bootstrap';
|
||||
import Main from './containers/Main';
|
||||
import TalkProvider from 'coral-framework/components/TalkProvider';
|
||||
import pluginsConfig from 'pluginsConfig';
|
||||
|
||||
async function main() {
|
||||
const context = await createContext({
|
||||
pluginsConfig,
|
||||
});
|
||||
render(
|
||||
<TalkProvider {...context}>
|
||||
<Main />
|
||||
</TalkProvider>,
|
||||
document.querySelector('#talk-login-container')
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,3 @@
|
||||
.bare {
|
||||
composes: buttonReset from "coral-framework/styles/reset.css";
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './BareButton.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
/**
|
||||
* BareButton is a button whose styling is stripped off to a minimum.
|
||||
* Can pass anchor=true to use `a` instead of `button`
|
||||
*/
|
||||
const BareButton = ({ anchor, className, ...props }) => {
|
||||
let Element = 'button';
|
||||
if (anchor) {
|
||||
Element = 'a';
|
||||
}
|
||||
return <Element {...props} className={cn(styles.bare, className)} />;
|
||||
};
|
||||
|
||||
BareButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
anchor: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default BareButton;
|
||||
@@ -10,6 +10,7 @@
|
||||
line-height: 22px;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -28,3 +28,4 @@ export { default as Label } from './components/Label';
|
||||
export { default as FlagLabel } from './components/FlagLabel';
|
||||
export { default as Dropdown } from './components/Dropdown';
|
||||
export { default as Option } from './components/Option';
|
||||
export { default as BareButton } from './components/BareButton';
|
||||
|
||||
@@ -44,6 +44,10 @@ const CONFIG = {
|
||||
// fetching again.
|
||||
SETTINGS_CACHE_TIME: ms(process.env.TALK_SETTINGS_CACHE_TIME || '1hr'),
|
||||
|
||||
// ALLOW_NO_LIMIT_QUERIES enables some queries to specify a limit of -1 to
|
||||
// request all of the records. Otherwise, minimum limits of 0 are enforced.
|
||||
ALLOW_NO_LIMIT_QUERIES: process.env.TALK_ALLOW_NO_LIMIT_QUERIES === 'TRUE',
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// JWT based configuration
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -84,23 +84,3 @@ TALK_JWT_SECRET=jX9y8G2ApcVLwyL{$6s3
|
||||
Be default, we sign our tokens with HMAC using a SHA-256 hash algorithm. If you
|
||||
want to change the signing algorithm, or use multiple signing/verifying keys,
|
||||
refer to our [Advanced Configuration]({{ "/advanced-configuration/" | relative_url }}) documentation.
|
||||
|
||||
## TALK_FACEBOOK_APP_ID
|
||||
|
||||
The Facebook App ID for your Facebook Login enabled app. You can learn more
|
||||
about getting a Facebook App ID at the
|
||||
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
|
||||
or by visiting the
|
||||
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
|
||||
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||
enabled.
|
||||
|
||||
## TALK_FACEBOOK_APP_SECRET
|
||||
|
||||
The Facebook App Secret for your Facebook Login enabled app. You can learn more
|
||||
about getting a Facebook App Secret at the
|
||||
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
|
||||
or by visiting the
|
||||
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
|
||||
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||
enabled.
|
||||
|
||||
@@ -57,6 +57,26 @@ When `TRUE`, it will not mount the static asset serving routes on the router.
|
||||
This is used primarily in conjunction with [TALK_STATIC_URI](#talk_static_uri){: .param}
|
||||
when the static assets are being hosted on an external domain. (Default `FALSE`)
|
||||
|
||||
## TALK_FACEBOOK_APP_ID
|
||||
|
||||
The Facebook App ID for your Facebook Login enabled app. You can learn more
|
||||
about getting a Facebook App ID at the
|
||||
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
|
||||
or by visiting the
|
||||
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
|
||||
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||
enabled.
|
||||
|
||||
## TALK_FACEBOOK_APP_SECRET
|
||||
|
||||
The Facebook App Secret for your Facebook Login enabled app. You can learn more
|
||||
about getting a Facebook App Secret at the
|
||||
[Facebook Developers Portal](https://developers.facebook.com){:target="_blank"}
|
||||
or by visiting the
|
||||
[Creating an App ID](https://developers.facebook.com/docs/apps/register){:target="_blank"}
|
||||
guide. This is only required while the `talk-plugin-facebook-auth` plugin is
|
||||
enabled.
|
||||
|
||||
## TALK_HELMET_CONFIGURATION
|
||||
|
||||
A JSON string representing the configuration passed to the
|
||||
@@ -303,6 +323,8 @@ the websocket to keep the socket alive, parsed by
|
||||
|
||||
## TALK_RECAPTCHA_PUBLIC
|
||||
|
||||
Setting a reCAPTCHA Public and Secret key will enable and require reCAPTCHA upon multiple failed login attempts.
|
||||
|
||||
Client secret used for enabling reCAPTCHA powered logins. If
|
||||
[TALK_RECAPTCHA_SECRET](#talk_recaptcha_secret){: .param} and
|
||||
[TALK_RECAPTCHA_PUBLIC](#talk_recaptcha_public){: .param} are not provided it will instead
|
||||
@@ -486,4 +508,34 @@ Used to set the key for use with
|
||||
[Apollo Engine](https://www.apollographql.com/engine/){:target="_blank"} for
|
||||
tracing of GraphQL requests.
|
||||
|
||||
**Note: Apollo Engine is a premium service, charges may apply.**
|
||||
**Note: Apollo Engine is a premium service, charges may apply.**
|
||||
|
||||
## ALLOW_NO_LIMIT_QUERIES
|
||||
|
||||
Setting this to `TRUE` will allow queries to execute without a limit (returns
|
||||
all documents). This introduces a significant performance regression, and should
|
||||
be used with caution. (Default `FALSE`)
|
||||
|
||||
## TALK_ADDTL_COMMENTS_ON_LOAD_MORE
|
||||
|
||||
This is a **Build Variable** and must be consumed during build. If using the
|
||||
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
|
||||
image you can specify it with `--build-arg TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10`.
|
||||
|
||||
Specifies the number of additional comments to load when a user clicks `Load More`. (Default `10`)
|
||||
|
||||
## TALK_ASSET_COMMENTS_LOAD_DEPTH
|
||||
|
||||
This is a **Build Variable** and must be consumed during build. If using the
|
||||
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
|
||||
image you can specify it with `--build-arg TALK_ASSET_COMMENTS_LOAD_DEPTH=10`.
|
||||
|
||||
Specifies the initial number of comments to load for an asset. (Default `10`)
|
||||
|
||||
## TALK_REPLY_COMMENTS_LOAD_DEPTH
|
||||
|
||||
This is a **Build Variable** and must be consumed during build. If using the
|
||||
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
|
||||
image you can specify it with `--build-arg TALK_REPLY_COMMENTS_LOAD_DEPTH=3`.
|
||||
|
||||
Specifies the initial replies to load for a comment. (Default `3`)
|
||||
@@ -103,3 +103,18 @@ Source: [plugins/talk-plugin-subscriber](https://github.com/coralproject/talk/tr
|
||||
Enables a `Subscriber` badge to be added to comments where the author has the
|
||||
`SUBSCRIBER` tag. This must match with a custom auth integration that adds the
|
||||
tag to the users that are subscribed to the service.
|
||||
|
||||
## talk-plugin-akismet
|
||||
|
||||
Source: [plugins/talk-plugin-akismet](https://github.com/coralproject/talk/tree/master/plugins/talk-plugin-akismet){:target="_blank"}
|
||||
|
||||
Enables spam detection from [Akismet](https://akismet.com/). Comments will be passed to the Akismet API for spam detection. If a comment
|
||||
is determined to be spam, it will prompt the user, indicating that the comment might be considered spam. If the user continues after this
|
||||
point with the still spam-like comment, the comment will be reported as containing spam, and sent for moderator approval.
|
||||
|
||||
**Note: [Akismet](https://akismet.com/) is a premium service, charges may apply.**
|
||||
|
||||
Configuration:
|
||||
|
||||
- `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/`
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ module.exports = {
|
||||
// Schema is created already, so just include it.
|
||||
schema,
|
||||
|
||||
// Load in the new context here, this'll create the loaders + mutators for
|
||||
// Load in the new context here, this will create the loaders + mutators for
|
||||
// the lifespan of this request.
|
||||
context: new Context(req),
|
||||
|
||||
|
||||
+158
-28
@@ -1,54 +1,184 @@
|
||||
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, isNil } = 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 =>
|
||||
!isNil(action_counts)
|
||||
? Object.keys(action_counts).map(action_type => ({
|
||||
count: action_counts[action_type],
|
||||
action_type: action_type.toUpperCase(),
|
||||
}))
|
||||
: [];
|
||||
|
||||
/**
|
||||
* getUserActions will get the actions made by the user for this specific
|
||||
* item.
|
||||
*
|
||||
* @param {Object} ctx the graph context of the request
|
||||
* @param {Object} item the item that we're getting the actions for
|
||||
*/
|
||||
async function getUserActions(ctx, { action_counts, id }) {
|
||||
const { loaders: { Actions } } = ctx;
|
||||
|
||||
// Get the total count for all action types.
|
||||
const totalActionCount = reduce(
|
||||
action_counts,
|
||||
(total, count) => total + count,
|
||||
0
|
||||
);
|
||||
|
||||
// Check to see if there are any user actions to get.
|
||||
const hasUserActions = ctx.user && totalActionCount > 0;
|
||||
if (!hasUserActions) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Possibly get the list of user actions completed by the user. This will be
|
||||
// used later to join together with the action summaries to provide context.
|
||||
const userActions = await Actions.getAuthoredByID.load(id);
|
||||
if (userActions.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Group the user actions in the same way that the action counts are
|
||||
// grouped. This will let us extract it easy.
|
||||
return reduce(
|
||||
groupBy(userActions, ({ action_type, group_id }) =>
|
||||
(group_id ? `${action_type}_${group_id}` : action_type).toUpperCase()
|
||||
),
|
||||
(allUserActions, userActions, actionType) =>
|
||||
merge(allUserActions, { [actionType]: first(userActions) }),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
// This will match any action count that is specific for a group id.
|
||||
const nonGroupIDTest = /^([A-Z]+)_([A-Z_]+)$/;
|
||||
|
||||
/**
|
||||
* resolveActionSummariesForItem will resolve the action summaries for an item.
|
||||
*
|
||||
* @param {Object} ctx the graph context of the request
|
||||
* @param {Object} item the item that we are resolving an action summary for
|
||||
*/
|
||||
async function resolveActionSummariesForItem(ctx, { id, action_counts }) {
|
||||
// Cache all those entries for which we got the group id of, because we
|
||||
// don't want to include them twice.
|
||||
const groupIDCache = {};
|
||||
|
||||
// Get the user actions for this specific item.
|
||||
const groupedUserActions = await getUserActions(ctx, { id, action_counts });
|
||||
|
||||
// Generate the action summaries for the item.
|
||||
return iterateActionCounts(action_counts).reduce(
|
||||
(actionTypeList, { count, action_type }) => {
|
||||
// Get the current user's actions (if they have any).
|
||||
const current_user = get(groupedUserActions, action_type, null);
|
||||
|
||||
// Check to see if this is a action without a corresponding group id.
|
||||
if (nonGroupIDTest.test(action_type)) {
|
||||
// This action type does have a group id associated with it.
|
||||
const results = nonGroupIDTest.exec(action_type);
|
||||
const groupActionType = results[1];
|
||||
const groupID = results[2];
|
||||
|
||||
// Purge out the summary if it already exists, and mark that this
|
||||
// group id has been found so we don't include it in the future.
|
||||
remove(
|
||||
actionTypeList,
|
||||
({ action_type }) => action_type === groupActionType
|
||||
);
|
||||
groupIDCache[groupActionType] = true;
|
||||
|
||||
// Push the new entry in.
|
||||
actionTypeList.push({
|
||||
action_type: groupActionType,
|
||||
group_id: groupID,
|
||||
count,
|
||||
current_user,
|
||||
});
|
||||
} else {
|
||||
// This does not have a group id. Check to see if this group id
|
||||
// already has an specific (group id) entry.
|
||||
if (groupIDCache[action_type]) {
|
||||
// It does. Don't add anything.
|
||||
return actionTypeList;
|
||||
}
|
||||
|
||||
// It does not, add the entry.
|
||||
actionTypeList.push({
|
||||
action_type,
|
||||
group_id: null,
|
||||
count,
|
||||
current_user,
|
||||
});
|
||||
}
|
||||
|
||||
return actionTypeList;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the action summaries for a set of items.
|
||||
*
|
||||
* @param {Object} ctx the graph context of the request
|
||||
* @param {Array<Object>} items the items that should have their items looked up for
|
||||
*/
|
||||
const genActionSummariesByItem = async (ctx, items) => {
|
||||
// This is designed to match the action_counts value that is embedded on
|
||||
// documents which cache action counts. For users that are not logged in, we
|
||||
// don't need to hit the actions collection at all!
|
||||
|
||||
// We will literate over all the items that we're comparing.
|
||||
return items.map(item => resolveActionSummariesForItem(ctx, item));
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
* @param {Object} ctx the context of the GraphQL request
|
||||
* @return {Object} object of loaders
|
||||
*/
|
||||
module.exports = context => ({
|
||||
module.exports = ctx => ({
|
||||
Actions: {
|
||||
getByID: new DataLoader(ids => genActionsByItemID(context, ids)),
|
||||
getSummariesByItemID: new DataLoader(ids =>
|
||||
genActionSummariessByItemID(context, ids)
|
||||
getByID: new DataLoader(ids => genActionsByItemID(ctx, ids)),
|
||||
getSummariesByItem: new DataLoader(
|
||||
items => genActionSummariesByItem(ctx, items),
|
||||
{ cacheKeyFn: ({ id }) => id }
|
||||
),
|
||||
getByTypes: ({ action_type, item_type }) =>
|
||||
getItemIdsByActionTypeAndItemType(context, action_type, item_type),
|
||||
getAuthoredByID: new DataLoader(ids => genActionsAuthoredWithID(ctx, ids)),
|
||||
},
|
||||
});
|
||||
|
||||
+44
-43
@@ -4,7 +4,10 @@ const {
|
||||
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS,
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
} = require('../../perms/constants');
|
||||
const { CACHE_EXPIRY_COMMENT_COUNT } = require('../../config');
|
||||
const {
|
||||
CACHE_EXPIRY_COMMENT_COUNT,
|
||||
ALLOW_NO_LIMIT_QUERIES,
|
||||
} = require('../../config');
|
||||
const ms = require('ms');
|
||||
const sc = require('snake-case');
|
||||
|
||||
@@ -148,12 +151,11 @@ const getCommentCountByQuery = (ctx, options) => {
|
||||
* @param {Object} params the params from the client describing the query
|
||||
*/
|
||||
const getStartCursor = (ctx, nodes, { cursor, sortBy }) => {
|
||||
switch (sortBy) {
|
||||
case 'CREATED_AT':
|
||||
return nodes.length ? nodes[0].created_at : null;
|
||||
case 'REPLIES':
|
||||
// The cursor is the start! This is using numeric pagination.
|
||||
return cursor != null ? cursor : 0;
|
||||
if (sortBy === 'CREATED_AT') {
|
||||
return nodes.length ? nodes[0].created_at : null;
|
||||
} else if (sortBy === 'REPLIES') {
|
||||
// The cursor is the start! This is using numeric pagination.
|
||||
return cursor != null ? cursor : 0;
|
||||
}
|
||||
|
||||
const SORT_KEY = sortBy.toLowerCase();
|
||||
@@ -181,11 +183,10 @@ const getStartCursor = (ctx, nodes, { cursor, sortBy }) => {
|
||||
* @param {Object} params the params from the client describing the query
|
||||
*/
|
||||
const getEndCursor = (ctx, nodes, { cursor, sortBy }) => {
|
||||
switch (sortBy) {
|
||||
case 'CREATED_AT':
|
||||
return nodes.length ? nodes[nodes.length - 1].created_at : null;
|
||||
case 'REPLIES':
|
||||
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
|
||||
if (sortBy === 'CREATED_AT') {
|
||||
return nodes.length ? nodes[nodes.length - 1].created_at : null;
|
||||
} else if (sortBy === 'REPLIES') {
|
||||
return nodes.length ? (cursor != null ? cursor : 0) + nodes.length : null;
|
||||
}
|
||||
|
||||
const SORT_KEY = sortBy.toLowerCase();
|
||||
@@ -212,36 +213,33 @@ const getEndCursor = (ctx, nodes, { cursor, sortBy }) => {
|
||||
* @param {Object} params the params from the client describing the query
|
||||
*/
|
||||
const applySort = (ctx, query, { cursor, sortOrder, sortBy }) => {
|
||||
switch (sortBy) {
|
||||
case 'CREATED_AT': {
|
||||
if (cursor) {
|
||||
if (sortOrder === 'DESC') {
|
||||
query = query.where({
|
||||
created_at: {
|
||||
$lt: cursor,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
query = query.where({
|
||||
created_at: {
|
||||
$gt: cursor,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (sortBy === 'CREATED_AT') {
|
||||
if (cursor) {
|
||||
if (sortOrder === 'DESC') {
|
||||
query = query.where({
|
||||
created_at: {
|
||||
$lt: cursor,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
query = query.where({
|
||||
created_at: {
|
||||
$gt: cursor,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return query.sort({ created_at: sortOrder === 'DESC' ? -1 : 1 });
|
||||
}
|
||||
case 'REPLIES': {
|
||||
if (cursor) {
|
||||
query = query.skip(cursor);
|
||||
}
|
||||
|
||||
return query.sort({
|
||||
reply_count: sortOrder === 'DESC' ? -1 : 1,
|
||||
created_at: sortOrder === 'DESC' ? -1 : 1,
|
||||
});
|
||||
return query.sort({ created_at: sortOrder === 'DESC' ? -1 : 1 });
|
||||
} else if (sortBy === 'REPLIES') {
|
||||
if (cursor) {
|
||||
query = query.skip(cursor);
|
||||
}
|
||||
|
||||
return query.sort({
|
||||
reply_count: sortOrder === 'DESC' ? -1 : 1,
|
||||
created_at: sortOrder === 'DESC' ? -1 : 1,
|
||||
});
|
||||
}
|
||||
|
||||
const SORT_KEY = sortBy.toLowerCase();
|
||||
@@ -280,7 +278,7 @@ const executeWithSort = async (
|
||||
query = applySort(ctx, query, { cursor, sortOrder, sortBy });
|
||||
|
||||
// Apply the limit (if it exists, as it's applied universally).
|
||||
if (limit) {
|
||||
if (limit >= 0) {
|
||||
query = query.limit(limit + 1);
|
||||
}
|
||||
|
||||
@@ -290,7 +288,7 @@ const executeWithSort = async (
|
||||
// The hasNextPage is always handled the same (ask for one more than we need,
|
||||
// if there is one more, than there is more).
|
||||
let hasNextPage = false;
|
||||
if (limit && nodes.length > limit) {
|
||||
if (limit >= 0 && nodes.length > limit) {
|
||||
// There was one more than we expected! Set hasNextPage = true and remove
|
||||
// the last item from the array that we requested.
|
||||
hasNextPage = true;
|
||||
@@ -302,11 +300,9 @@ const executeWithSort = async (
|
||||
return {
|
||||
startCursor: getStartCursor(ctx, nodes, {
|
||||
cursor,
|
||||
sortOrder,
|
||||
sortBy,
|
||||
limit,
|
||||
}),
|
||||
endCursor: getEndCursor(ctx, nodes, { cursor, sortOrder, sortBy, limit }),
|
||||
endCursor: getEndCursor(ctx, nodes, { cursor, sortBy }),
|
||||
hasNextPage,
|
||||
nodes,
|
||||
};
|
||||
@@ -338,6 +334,11 @@ const getCommentsByQuery = async (
|
||||
) => {
|
||||
let comments = CommentModel.find();
|
||||
|
||||
// Enforce that the limit must be gte 0 if this option is not true.
|
||||
if (!ALLOW_NO_LIMIT_QUERIES && limit < 0) {
|
||||
throw new Error('cannot query for limit < 0');
|
||||
}
|
||||
|
||||
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
|
||||
// special privileges.
|
||||
if (
|
||||
|
||||
@@ -40,12 +40,12 @@ const Comment = {
|
||||
|
||||
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);
|
||||
|
||||
@@ -10,8 +10,8 @@ const {
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const User = {
|
||||
action_summaries({ id }, _, { loaders: { Actions } }) {
|
||||
return Actions.getSummariesByItemID.load(id);
|
||||
action_summaries(user, _, { loaders: { Actions } }) {
|
||||
return Actions.getSummariesByItem.load(user);
|
||||
},
|
||||
actions({ id }, _, { user, loaders: { Actions } }) {
|
||||
// Only return the actions if the user is not an admin.
|
||||
|
||||
+80
-1
@@ -12,11 +12,26 @@ da:
|
||||
note_reject_comment: "Hvis du banner denne bruger vil det også placere denne kommentar i den afviste kø"
|
||||
note_ban_user: "Hvis du banner denne bruger vil det forhindre dem i at redigere kommentarer eller fjerne noget"
|
||||
yes_ban_user: "Ja, Ban Bruger"
|
||||
write_a_message: "Write a message"
|
||||
send: "Send"
|
||||
notify_ban_headline: "Notify the user of ban"
|
||||
notify_ban_description: "This will notify the user by email that they have been banned from the community"
|
||||
email_message_ban: "Dear {0},\n\nSomeone with access to your account has violated our community guidelines. As a result, your account has been banned. You will no longer be able to comment, like or report comments. if you think this has been done in error, please contact our community team."
|
||||
bio_offensive: "Denne biografi er stødende"
|
||||
cancel: "Afbryd"
|
||||
confirm_email:
|
||||
click_to_confirm: "Click below to confirm your email address"
|
||||
confirm: "Confirm"
|
||||
password_reset:
|
||||
set_new_password: "Change Your Password"
|
||||
new_password: "New Password"
|
||||
new_password_help: "Password must be at least 8 characters"
|
||||
confirm_new_password: "Confirm New Password"
|
||||
change_password: "Change Password"
|
||||
characters_remaining: "tegn tilbage"
|
||||
comment:
|
||||
anon: "Anonym"
|
||||
undo_reject: "Undo"
|
||||
ban_user: "Ban Bruger"
|
||||
comment: "Skriv en kommentar"
|
||||
edited: "Redigeret"
|
||||
@@ -46,6 +61,11 @@ da:
|
||||
reaction: "reaktion"
|
||||
reactions: "reaktioner"
|
||||
story: "Historie"
|
||||
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}'
|
||||
community:
|
||||
account_creation_date: "Oprettelsedato for konto"
|
||||
active: "Aktive"
|
||||
@@ -75,6 +95,7 @@ da:
|
||||
status: "Status"
|
||||
username_and_email: "Brugernavn eller email-adresse"
|
||||
yes_ban_user: "Ja Ban Bruger"
|
||||
commenter: "Commenter"
|
||||
configure:
|
||||
apply: "Anvend"
|
||||
banned_word_text: "Kommentarer, der indeholder disse ord eller sætninger (Ikke følsom over for store eller små bogstaver) fjernes automatisk fra kommentarstrømmen. Indtast et ord og tryk på Enter eller Tab for at tilføje. Indsæt eventuelt en kommasepareret liste."
|
||||
@@ -161,7 +182,14 @@ da:
|
||||
edit_window_timer_prefix: "Luk vindue: "
|
||||
second: "sekund"
|
||||
seconds_plural: "sekunder"
|
||||
minute: "minute"
|
||||
minutes_plural: "minutes"
|
||||
email:
|
||||
suspended:
|
||||
subject: "Your account has been suspended"
|
||||
banned:
|
||||
subject: "Your account has been banned"
|
||||
body: "In accordance with The Coral Project’s community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community."
|
||||
confirm:
|
||||
has_been_requested: "Der er anmodet om en e-mail bekræftelse for følgende konto:"
|
||||
to_confirm: "For at bekræfte kontoen, skal du besøge følgende link:"
|
||||
@@ -175,13 +203,18 @@ da:
|
||||
embedlink:
|
||||
copy: "Kopier til udklipsholder"
|
||||
error:
|
||||
COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist."
|
||||
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
|
||||
PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid."
|
||||
COMMENT_TOO_SHORT: "Din kommentar skal indeholde noget"
|
||||
NOT_AUTHORIZED: "Du har ikke tilladelse til at udføre denne handling."
|
||||
NO_SPECIAL_CHARACTERS: "Brugernavne kan kun indeholder bogstaver og _"
|
||||
PASSWORD_LENGTH: "Adgangskoden er for kort"
|
||||
PROFANITY_ERROR: "Brugernavne må ikke inholde stødende indhold. Kontakt venligst administratoren, hvis du mener at dette er en fejl."
|
||||
RATE_LIMIT_EXCEEDED: "Rate limit exceeded"
|
||||
USERNAME_IN_USE: "Brugernavnet er allerede i brug"
|
||||
USERNAME_REQUIRED: "Du skal indtaste et brugernavn"
|
||||
EMAIL_NOT_VERIFIED: "E-mail address not verified"
|
||||
EDIT_WINDOW_ENDED: "Du kan ikke længere redigere denne kommentar. Tidsvinduet for at gøre det er udløbet."
|
||||
EDIT_USERNAME_NOT_AUTHORIZED: "Du har ikke tilladelse til at opdatere dit brugernavn."
|
||||
SAME_USERNAME_PROVIDED: "Du skal indsende et andet brugernavn."
|
||||
@@ -193,6 +226,7 @@ da:
|
||||
NOT_FOUND: "Ressource ikke fundet"
|
||||
ALREADY_EXISTS: "Ressource eksisterer allerede"
|
||||
INVALID_ASSET_URL: "Aktivitetswebadresse er ugyldig"
|
||||
CANNOT_IGNORE_STAFF: "Cannot ignore Staff members."
|
||||
email: "Ikke en gylding e-mail"
|
||||
confirm_password: "Kodeordene matcher ikke. Tjek venligst igen."
|
||||
network_error: "Det lykkedes ikke at oprette forbindelse til serveren. Tjek din internetforbindelse og prøv igen."
|
||||
@@ -202,6 +236,7 @@ da:
|
||||
password: "Adgangskoden skal være mindst 8 tegn"
|
||||
username: "Brugernavne kan kun indeholde bogstaver og _"
|
||||
unexpected: "Der opstod en uventet fejl. Undskyld!"
|
||||
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
|
||||
flag_comment: "Rapportér kommentar"
|
||||
flag_reason: "Årsag til rapportering (Valgfrit)"
|
||||
flag_username: "Rapporter brugernavn"
|
||||
@@ -220,6 +255,8 @@ da:
|
||||
error: "Brugernavne kan kun indeholde bogstaver og _"
|
||||
label: "Nyt brugernavn"
|
||||
msg: "Din konto er midlertidigt suspenderet, fordi dit brugernavn er blevet anset for upassende. For at gendanne din konto skal du indtaste et nyt brugernavn. Kontakt os venligst, hvis du har spørgsmål."
|
||||
changed_name:
|
||||
msg: "Your username change is under review by our moderation team."
|
||||
my_comments: "Mine kommentarer"
|
||||
my_profile: "Min profil"
|
||||
new_count: "Se {0} mere {1}"
|
||||
@@ -237,6 +274,24 @@ da:
|
||||
loading_results: "Indlæser resultater"
|
||||
marketing: "Dette ligner en annonce/markedsføring"
|
||||
moderate_this_stream: "Moderer denne strøm"
|
||||
flags:
|
||||
reasons:
|
||||
user:
|
||||
username_offensive: "Offensive"
|
||||
username_nolike: "Dislike"
|
||||
username_impersonating: "Impersonation"
|
||||
username_spam: "Spam"
|
||||
username_other: "Other"
|
||||
comment:
|
||||
comment_offensive: "Offensive"
|
||||
comment_spam: "Spam"
|
||||
comment_noagree: "Disagree"
|
||||
comment_other: "Other"
|
||||
suspect_word: "Suspect Word"
|
||||
banned_word: "Banned Word"
|
||||
body_count: "Body exceeds max length"
|
||||
trust: "Trust"
|
||||
links: "Link"
|
||||
modqueue:
|
||||
account: "konto flag"
|
||||
actions: "Handlinger"
|
||||
@@ -246,6 +301,7 @@ da:
|
||||
notify_accepted: "{0} accepteret kommentar {1}"
|
||||
notify_rejected: "{0} afvist kommentar {1}"
|
||||
notify_flagged: "{0} flagget kommentar {1}"
|
||||
notify_reset: '{0} reset status of comment "{1}"'
|
||||
approve: "Godkend"
|
||||
approved: "Godkendt"
|
||||
ban_user: "Ban"
|
||||
@@ -254,6 +310,7 @@ da:
|
||||
empty_queue: "Ikke flere kommentarer til at moderer! Du har indhentet det hele. Gå ud og tag noget ☕️"
|
||||
flagged: "flagged"
|
||||
reported: "Rapporteret"
|
||||
jump_to_queue: "Jump to specific queue"
|
||||
less_detail: "Færre detajler"
|
||||
likes: "Syntes godt om"
|
||||
million: "M"
|
||||
@@ -264,6 +321,7 @@ da:
|
||||
newest_first: "Nyeste først"
|
||||
navigation: "Navigation"
|
||||
next_comment: "Gå til næste kommentar"
|
||||
next_queue: "Switch queues"
|
||||
oldest_first: "Ældste Først"
|
||||
premod: "pre-mod"
|
||||
prev_comment: "Gå til den forrige kommentar"
|
||||
@@ -275,15 +333,18 @@ da:
|
||||
shortcuts: "Genveje"
|
||||
show_shortcuts: "Vis genveje"
|
||||
singleview: "Zen mode"
|
||||
sort: "Sort"
|
||||
thismenu: "Åben denne menu"
|
||||
thousand: "k"
|
||||
try_these: "Prøv disse"
|
||||
toggle_search: "Open search"
|
||||
view_more_shortcuts: "Se flere genveje"
|
||||
my_comment_history: "Min kommentar Historie"
|
||||
name: "Navn"
|
||||
no_agree_comment: "Jeg er ikke enig med denne kommentar"
|
||||
no_like_bio: "Jeg kan ikke lide denne biografi"
|
||||
no_like_username: "Jeg kan ikke lide dette brugernavn"
|
||||
already_flagged_username: "You have already flagged this username."
|
||||
other: "Andet"
|
||||
permalink: "Delen"
|
||||
personal_info: "Denne kommentar afslører personligt identificerbare oplysninger"
|
||||
@@ -306,7 +367,9 @@ da:
|
||||
stream:
|
||||
all_comments: "Alle kommentarer"
|
||||
temporarily_suspended: "I overensstemmelse med {0}'s retningslinjer for fællesskabet er din konto midlertidigt suspenderet. Venligst tilslut dig samtalen {1}."
|
||||
comment_not_found: "Kommentar blev ikke fundet"
|
||||
comment_not_found: "Denne kommentar er blevet fjernet eller findes ikke."
|
||||
no_comments: "There are no comments yet, why don’t you write one?"
|
||||
no_comments_and_closed: "There were no comments on this article."
|
||||
step_1_header: "Rapportér et problem"
|
||||
step_2_header: "Hjælp os med at forstå"
|
||||
step_3_header: "Tak for din indsats"
|
||||
@@ -356,6 +419,19 @@ da:
|
||||
bio_flags: "flag for denne bio"
|
||||
user_bio: "Bruger Biografi"
|
||||
username_flags: "flag for dette brugernavn"
|
||||
user_detail:
|
||||
remove_suspension: "Remove Suspension"
|
||||
suspend: "Suspend User"
|
||||
remove_ban: "Remove Ban"
|
||||
ban: "Ban User"
|
||||
member_since: "Member Since"
|
||||
email: "Email"
|
||||
total_comments: "Total Comments"
|
||||
reject_rate: "Reject Rate"
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
account_history: "Account History"
|
||||
user_impersonating: "Denne bruger efterligner"
|
||||
user_no_comment: "Du har aldrig efterladt en kommentar. Deltag i samtalen"
|
||||
username_offensive: "Dette brugernavn er stødende"
|
||||
@@ -382,3 +458,6 @@ da:
|
||||
description: "Tak fordi du installerede Talk! Vi sendte en email for at bekræfte din e-mail-adresse. Mens du færdigøre oprettelsen af kontoen, kan du begynde at engagere med dine læsere."
|
||||
launch: "Lancere Talk"
|
||||
close: "Luk dette installations program"
|
||||
admin_sidebar:
|
||||
view_options: "View Options"
|
||||
sort_comments: "Sort Comments"
|
||||
|
||||
+462
@@ -0,0 +1,462 @@
|
||||
de:
|
||||
your_account_has_been_suspended: Ihr Zugang wurde vorübergehend gesperrt.
|
||||
your_account_has_been_banned: Ihr Zugang wurde gesperrt.
|
||||
your_username_has_been_rejected: Ihr Zugang wurde vorübergehend gesperrt, da Ihr Nutzername als unangemessen eingestuft wurde. Um Ihren Zugang wieder herzustellen, geben Sie bitte einen neuen Nutzernamen ein.
|
||||
embed_comments_tab: Kommentare
|
||||
bandialog:
|
||||
are_you_sure: "Sind Sie sich sicher, dass Sie {0} sperren möchten?"
|
||||
ban_user: "Nutzer sperren?"
|
||||
banned_user: "Gesperrter Nutzer"
|
||||
cancel: "Beenden"
|
||||
note: "Notiz: {0}"
|
||||
note_reject_comment: "Das Sperren dieses Nutzers wird auch diesen Kommentar in die Abgelehnt-Liste verschieben."
|
||||
note_ban_user: "Nach dem Sperren wird dieser Nutzer weder Kommentare schreiben, noch anderweitig mit der Community interagieren können."
|
||||
yes_ban_user: "Ja, Nutzer sperren"
|
||||
write_a_message: "Nachricht schreiben"
|
||||
send: "Senden"
|
||||
notify_ban_headline: "Nutzer über Sperrung benachrichtigen"
|
||||
notify_ban_description: "Dies wird den Nutzer per E-Mail über den Ausschluss aus der Community informieren"
|
||||
email_message_ban: "Sehr geehrte/r {0},\n\nJemand mit Zugriff auf Ihr Konto hat gegen unsere Community-Richtlinien verstoßen. Daraufhin wurde Ihr Konto gesperrt. Sie können nun weder kommentieren noch anderweitig mit der Community interagieren. Falls Sie denken, dass es sich hierbei um einen Fehler handelt, nehmen Sie bitte Kontakt mit unseren Moderatoren auf."
|
||||
bio_offensive: "Diese Biographie ist unangemessen"
|
||||
cancel: "Abbrechen"
|
||||
confirm_email:
|
||||
click_to_confirm: "Unten klicken, um E-Mail-Adresse zu bestätigen"
|
||||
confirm: "Bestätigen"
|
||||
password_reset:
|
||||
set_new_password: "Passwort ändern"
|
||||
new_password: "Neues Passwort"
|
||||
new_password_help: "Das Passwort benötigt mindestens 8 Zeichen"
|
||||
confirm_new_password: "Neues Passwort bestätigen"
|
||||
change_password: "Passwort ändern"
|
||||
characters_remaining: "verbleibende Zeichen"
|
||||
comment:
|
||||
anon: "Anonym"
|
||||
undo_reject: "Rückgängig machen"
|
||||
ban_user: "Nutzer sperren"
|
||||
comment: "Kommentar verfassen…"
|
||||
edited: Bearbeitet
|
||||
flagged: "markiert"
|
||||
view_context: "Kontext ansehen"
|
||||
comment_box:
|
||||
post: "Senden"
|
||||
cancel: "Abbrechen"
|
||||
reply: "Antworten"
|
||||
comment: "Kommentar verfassen"
|
||||
name: "Name"
|
||||
comment_post_notif: "Ihr Kommentar wurde versandt."
|
||||
comment_post_notif_premod: "Vielen Dank für Ihren Kommentar. Unsere Moderatoren werden ihn in Kürze bearbeiten."
|
||||
comment_post_banned_word: "Ihr Kommentar enthält ein oder mehrere Wörter, die unzulässig sind, daher wird er nicht veröffentlicht. Falls Sie dies als ungerechtfertigt ansehen, nehmen Sie bitte Kontakt zu unseren Moderatoren auf."
|
||||
characters_remaining: "verbleibende Zeichen"
|
||||
comment_offensive: "Dieser Kommentar ist unangemessen"
|
||||
comment_singular: Kommentar
|
||||
comment_plural: Kommentare
|
||||
comment_post_banned_word: "Ihr Kommentar enthält ein oder mehrere Wörter, die unzulässig sind, daher wird er nicht veröffentlicht. Falls Sie dies als ungerechtfertigt ansehen, nehmen Sie bitte Kontakt zu unseren Moderatoren auf."
|
||||
comment_post_notif: "Ihr Kommentar wurde veröffentlicht."
|
||||
comment_post_notif_premod: "Vielen Dank für Ihren Kommentar. Unsere Moderatoren werden ihn in Kürze bearbeiten."
|
||||
common:
|
||||
copy: 'Kopieren'
|
||||
error: 'Ein Problem ist aufgetreten.'
|
||||
reply: 'antworten'
|
||||
replies: 'Antworten'
|
||||
reaction: 'Reaktion'
|
||||
reactions: 'Reaktionen'
|
||||
story: 'Artikel'
|
||||
flagged_usernames:
|
||||
notify_approved: '{0} hat Nutzername {1} freigegeben'
|
||||
notify_rejected: '{0} hat Nutzername {1} abgelehnt'
|
||||
notify_flagged: '{0} hat Nutzername {1} markiert'
|
||||
notify_changed: 'Nutzer {0} hat Nutzernamen nach {1} geändert'
|
||||
community:
|
||||
account_creation_date: "Datum der Kontoerstellung"
|
||||
active: Aktiv
|
||||
admin: Administrator
|
||||
ads_marketing: "Dies scheint Werbung zu sein"
|
||||
are_you_sure: "Sind Sie sich sicher, dass Sie {0} sperren möchten?"
|
||||
ban_user: "Nutzer sperren?"
|
||||
banned: Gesperrt
|
||||
banned_user: "Gesperrter Nutzer"
|
||||
cancel: Abbrechen
|
||||
dont_like_username: "Unsympathischer Nutzername"
|
||||
flaggedaccounts: "Gemeldete Nutzernamen"
|
||||
flags: Markierungen
|
||||
impersonating: "Identität unklar"
|
||||
loading: "Lädt Ergebnisse"
|
||||
moderator: Moderator
|
||||
newsroom_role: "Redaktionsrolle"
|
||||
no_flagged_accounts: "Die Liste der gemeldeten Nutzer ist aktuell leer."
|
||||
no_results: "Keine Nutzer mit entsprechendem Namen oder E-Mail-Adresse gefunden."
|
||||
offensive: "Unangemessen"
|
||||
other: Etwas anderes
|
||||
people: Nutzer
|
||||
role: "Rolle auswählen..."
|
||||
select_status: "Status wählen..."
|
||||
spam_ads: "Spam/Werbung"
|
||||
staff: "Mitarbeiter"
|
||||
status: Status
|
||||
username_and_email: "Nutzername und E-Mail"
|
||||
yes_ban_user: "Ja, Nutzer sperren"
|
||||
commenter: "Kommentator"
|
||||
configure:
|
||||
apply: Anwenden
|
||||
banned_word_text: "Kommentare, die diese Wörter oder Phrasen enthalten (unabhängig von Groß-/Kleinschreibung), werden automatisch aus dem Kommentarbereich entfernt. Geben Sie ein Wort ein und bestätigen Sie mit Eingabetaste oder Tab. Es ist auch möglich, einen komma-separierten Text einzufügen."
|
||||
banned_words_title: "Liste der unzulässigen Wörter"
|
||||
close: "Schließen"
|
||||
close_after: "Kommentarbereich schließen nach"
|
||||
close_stream: "Kommentarbereich schließen"
|
||||
close_stream_configuration: "Dieser Kommentarbereich ist momentan geschlossen. Durch das Öffnen dieses Kommentarbereichs können neue Kommentare abgegeben und veröffentlicht werden."
|
||||
closed_comments_desc: "Wird angezeigt, wenn der Kommentarbereich für den Artikel geschlossen ist."
|
||||
closed_comments_label: "Nachricht verfassen..."
|
||||
closed_stream_settings: "Mitteilung über Schließung"
|
||||
comment_count_error: "Geben Sie eine gültige Zahl an."
|
||||
comment_count_header: "Zeichenlimit für Kommentare festlegen"
|
||||
comment_count_text_post: Zeichen
|
||||
comment_count_text_pre: "Kommentar wird begrenzt auf"
|
||||
comment_settings: Einstellungen
|
||||
comment_stream: "Kommentarbereich"
|
||||
comment_stream_will_close: "Der Kommentarbereich wird geschlossen"
|
||||
community: Community
|
||||
configure: Einstellungen
|
||||
copy_and_paste: "Kopieren Sie diesen Code in Ihr CMS, um die Kommentarfunktion an entsprechender Stelle anzuzeigen."
|
||||
custom_css_url: "Benutzerdefinierte CSS-URL"
|
||||
custom_css_url_desc: "URL eines CSS-Stylesheets zum Überschreiben des Standard-Designs"
|
||||
days: Tage
|
||||
description: "Als Administrator können Sie die Einstellungen für den Kommentarbereich dieses Artikels anpassen:"
|
||||
domain_list_text: "Geben Sie Domains an, für die diese Talk-Instanz freigegeben werden soll, z.B. für lokale Test- oder Produktionsumgebungen (Bsp.: localhost:3000 staging.domain.com domain.com)."
|
||||
domain_list_title: "Zugelassene Domains"
|
||||
edit_comment_timeframe_heading: "Zeitlimit zur Bearbeitung von Kommentaren"
|
||||
edit_comment_timeframe_text_pre: "Kommentatoren haben"
|
||||
edit_comment_timeframe_text_post: "Sekunden Zeit, um ihre Kommentare zu bearbeiten."
|
||||
embed_comment_stream: "Kommentarbereich einbinden"
|
||||
enable_premod_links_text: "Moderatoren müssen jeden Kommentar, der einen Link enthält, freigeben, bevor er veröffentlicht wird."
|
||||
enable_pre_moderation: "Vormoderation aktivieren"
|
||||
enable_pre_moderation_text: "Moderatoren müssen jeden Kommentar freigeben, bevor er veröffentlicht wird."
|
||||
enable_premod_links: "Vormoderation für Kommentare mit Links aktivieren"
|
||||
enable_premod: "Vormoderation aktivieren"
|
||||
enable_premod_description: "Moderatoren müssen jeden Kommentar freigeben, bevor er veröffentlicht wird."
|
||||
enable_premod_links_description: "Moderatoren müssen jeden Kommentar, der einen Link enthält, freigeben, bevor er veröffentlicht wird."
|
||||
enable_questionbox: "Stellen Sie den Lesern eine Frage"
|
||||
enable_questionbox_description: "Diese Frage erscheint am Anfang des Kommentarbereichs. Regen Sie eine Diskussion an."
|
||||
hours: Stunden
|
||||
include_comment_stream: "Einleitung zum Kommentarbereich"
|
||||
include_comment_stream_desc: "Verfassen Sie eine Einleitung, die über jedem Kommentarbereich erscheint. Nützlich z.B. für Community-Richtlinien."
|
||||
include_text: "Fügen Sie Ihren Text hier ein."
|
||||
include_question_here: "Stellen Sie Ihre Frage hier:"
|
||||
moderate: Moderieren
|
||||
moderation_settings: "Moderations-Einstellungen"
|
||||
open: "Öffnen"
|
||||
open_stream: "Kommentarbereich öffnen"
|
||||
open_stream_configuration: "Dieser Kommentarbereich ist momentan geöffnet. Nach dem Schließen dieses Kommentarbereich wird es nicht mehr möglich sein, zu kommentieren. Bestehende Kommentare bleiben sichtbar."
|
||||
require_email_verification: "E-Mail-Bestätigung erforderlich"
|
||||
require_email_verification_text: "Neue Nutzer müssen ihre E-Mail-Adresse bestätigen."
|
||||
save_changes: "Änderungen speichern"
|
||||
shortcuts: Tastaturkürzel
|
||||
sign_out: "Abmelden"
|
||||
stories: Artikel
|
||||
stream_settings: "Einstellungen Kommentarbereich"
|
||||
suspect_word_title: "Liste verdächtiger Wörter"
|
||||
suspect_word_text: "Kommentare, die diese Wörter oder Phrasen enthalten (unabhängig von Groß-/Kleinschreibung), werden im Kommentarbereich markiert. Geben Sie ein Wort ein und bestätigen Sie mit Eingabetaste oder Tab. Es ist auch möglich, einen komma-separierten Text einzufügen."
|
||||
tech_settings: "Technische Einstellungen"
|
||||
title: "Kommentarbereich konfigurieren"
|
||||
weeks: Wochen
|
||||
wordlist: "Gesperrte Wörter"
|
||||
continue: "Fortfahren"
|
||||
createdisplay:
|
||||
check_the_form: "Ungültige Eingabe. Bitte prüfen Sie die Felder."
|
||||
continue: "Mit diesem Facebook-Nutzernamen fortfahren"
|
||||
error_create: "Fehler beim Ändern des Nutzernamens"
|
||||
fake_comment_body: "Dies ist ein Beispiel-Kommentar. Leser können im Kommentarbereich Meinungen und Gedanken mit der Redaktion austauschen."
|
||||
fake_comment_date: "Vor einer Minute"
|
||||
if_you_dont_change_your_name: "Wenn Sie Ihren Nutzernamen an diesem Punkt nicht ändern, wird Ihr Facebook-Name mit Ihren Kommentaren angezeigt."
|
||||
required_field: "Pflichtfeld"
|
||||
save: Speichern
|
||||
special_characters: "Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten"
|
||||
username: Nutzername
|
||||
write_your_username: "Nutzername bearbeiten"
|
||||
your_username: "Ihr Nutzername erscheint an jedem Ihrer veröffentlichten Kommentare."
|
||||
done: Fertig
|
||||
edit_comment:
|
||||
body_input_label: "Kommentar bearbeiten"
|
||||
save_button: "Änderungen speichern"
|
||||
edit_window_expired: "Sie können diesen Kommentar nicht mehr bearbeiten. Das Zeitfenster hierfür ist abgelaufen. Bitte schreiben Sie einen neuen Kommentar."
|
||||
edit_window_expired_close: "Schließen"
|
||||
edit_window_timer_prefix: "Verbleibende Zeit: "
|
||||
second: "Sekunde"
|
||||
seconds_plural: "Sekunden"
|
||||
minute: "Minute"
|
||||
minutes_plural: "Minuten"
|
||||
email:
|
||||
suspended:
|
||||
subject: "Ihr Benutzerkonto wurde vorübergehend gesperrt"
|
||||
banned:
|
||||
subject: "Ihr Benutzerkonto wurde gesperrt"
|
||||
body: "Entsprechend der Community-Richtlinien des Coral Project, wurde Ihr Benutzerkonto gesperrt. Sie dürfen ab sofort weder kommentieren noch an der Community teilnehmen."
|
||||
confirm:
|
||||
has_been_requested: "Für folgendes Nutzerkonto wurde eine E-Mail-Bestätigung angefordert:"
|
||||
to_confirm: "Um Ihr Nutzerkonto zu bestätigen, öffnen Sie bitte folgenden Link:"
|
||||
confirm_email: "E-Mail-Adresse bestätigen"
|
||||
if_you_did_not: "Sollten Sie diese E-Mail nicht angefordert haben, können Sie diese Nachricht ignorieren."
|
||||
subject: "Bestätigung der E-Mail-Adresse"
|
||||
password_reset:
|
||||
we_received_a_request: "Wir haben eine Anfrage erhalten, Ihr Passwort zurückzusetzen. Sollten Sie dies nicht angefordert haben, können Sie diese Nachricht ignorieren."
|
||||
if_you_did: "Falls doch,"
|
||||
please_click: "klicken Sie bitte hier zum Zurücksetzen"
|
||||
embedlink:
|
||||
copy: "In die Zwischenablage kopieren"
|
||||
error:
|
||||
COMMENT_PARENT_NOT_VISIBLE: "Der Kommentar, auf den Sie antworten möchten, wurde entfernt oder existiert nicht."
|
||||
EMAIL_VERIFICATION_TOKEN_INVALID: "Code zur E-Mail-Bestätigung ist ungültig."
|
||||
PASSWORD_RESET_TOKEN_INVALID: "Ihr Link zum Passwort zurücksetzen ist ungültig."
|
||||
COMMENT_TOO_SHORT: "Kommentare sollten mehr als ein Zeichen enthalten, bitte überprüfen Sie Ihren Kommentar und probieren Sie es erneut."
|
||||
NOT_AUTHORIZED: "Sie sind nicht berechtigt, diese Aktion auszuführen."
|
||||
NO_SPECIAL_CHARACTERS: "Benutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten"
|
||||
PASSWORD_LENGTH: "Passwort ist zu kurz"
|
||||
PROFANITY_ERROR: "Nutzernamen dürfen nicht anstößig sein. Bei Fragen, wenden Sie sich an einen Administrator."
|
||||
RATE_LIMIT_EXCEEDED: "Ratelimit überschritten"
|
||||
USERNAME_IN_USE: "Nutzername bereits vergeben"
|
||||
USERNAME_REQUIRED: "Nutzername muss angegeben werden"
|
||||
EMAIL_NOT_VERIFIED: "E-Mail-Adresse nicht bestätigt"
|
||||
EDIT_WINDOW_ENDED: "Sie können diesen Kommentar nicht mehr bearbeiten. Das Zeitfenster ist abgelaufen."
|
||||
EDIT_USERNAME_NOT_AUTHORIZED: "Sie haben keine Berechtigung, Ihren Nutzernamen zu ändern."
|
||||
SAME_USERNAME_PROVIDED: "Sie müssen einen anderen Nutzernamen wählen."
|
||||
EMAIL_IN_USE: "E-Mail-Adresse bereits vergeben"
|
||||
EMAIL_REQUIRED: "Eine E-Mail-Adresse ist erforderlich"
|
||||
LOGIN_MAXIMUM_EXCEEDED: "Sie haben zu häufig erfolglos versucht, sich anzumelden. Bitte warten Sie."
|
||||
PASSWORD_REQUIRED: "Passwort ist erforderlich"
|
||||
COMMENTING_CLOSED: "Kommentarbereich ist bereits geschlossen"
|
||||
NOT_FOUND: "Ressource nicht gefunden"
|
||||
ALREADY_EXISTS: "Ressource existiert bereits"
|
||||
INVALID_ASSET_URL: "Asset-URL ist ungültig"
|
||||
CANNOT_IGNORE_STAFF: "Mitarbeiter können nicht ignoriert werden."
|
||||
email: "E-Mail-Adresse ungültig"
|
||||
confirm_password: "Passwörter nicht identisch. Bitte erneut überprüfen"
|
||||
network_error: "Server-Verbindung fehlgeschlagen. Bitte überprüfen Sie ihre Internetverbindung und versuchen Sie es erneut."
|
||||
email_not_verified: "E-Mail-Adresse {0} nicht bestätigt."
|
||||
email_password: "E-Mail und/oder Passwort inkorrekt."
|
||||
organization_name: "Namen von Organisationen dürfen nur Buchstaben und Zahlen enthalten."
|
||||
password: "Passwort muss mindestens 8 Zeichen enthalten"
|
||||
username: "Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten"
|
||||
unexpected: "Unerwarteter Fehler aufgetreten. Es tut uns leid!"
|
||||
flag_comment: "Kommentar melden"
|
||||
flag_reason: "Grund der Meldung (optional)"
|
||||
flag_username: "Nutzername melden"
|
||||
framework:
|
||||
banned_account_header: "Ihr Konto ist aktuell gesperrt."
|
||||
banned_account_body: "Dies bedeutet, dass Sie weder Kommentare schreiben, noch auf andere Weise mit der Community interagieren können."
|
||||
comment: Kommentar
|
||||
comment_is_ignored: "Dieser Kommentar ist nicht sichtbar, da Sie den Nutzer ignorieren."
|
||||
comment_is_rejected: "Sie haben diesen Kommentar abgelehnt."
|
||||
comment_is_hidden: "Dieser Kommentar ist nicht verfügbar."
|
||||
comments: Kommentare
|
||||
configure_stream: "Konfigurieren"
|
||||
content_not_available: "Dieser Inhalt ist nicht verfügbar"
|
||||
edit_name:
|
||||
button: Senden
|
||||
error: "Nutzernamen dürfen nur Buchstaben, Zahlen und _ enthalten"
|
||||
label: "Neuer Nutzername"
|
||||
msg: "Ihr Konto ist vorübergehend gesperrt, da Ihr Nutzername als unangemessen eingestuft wurde. Um Ihr Konto wieder herzustellen, geben Sie bitte einen neuen Nutzernamen ein. Bei Fragen, kontaktieren Sie uns bitte."
|
||||
changed_name:
|
||||
msg: "Die Änderung Ihres Nutzernamens wird von unseren Moderatoren geprüft."
|
||||
my_comments: "Meine Kommentare"
|
||||
my_profile: "Mein Profil"
|
||||
new_count: "{0} {1} mehr anzeigen"
|
||||
profile: Profil
|
||||
show_all_comments: "Alle Kommentare anzeigen"
|
||||
success_bio_update: "Ihre Biographie wurde aktualisiert"
|
||||
success_name_update: "Ihr Nutzername wurde aktualisiert"
|
||||
success_update_settings: "Ihre Änderungen wurden für den Kommentarbereich dieses Artikels angewandt"
|
||||
show_all_replies: Alle Antworten anzeigen
|
||||
show_more_replies: Weitere Antworten anzeigen
|
||||
view_more_comments: "Weitere Kommentare anzeigen"
|
||||
view_reply: "Antwort anzeigen"
|
||||
from_settings_page: "Sie können Ihren Kommentarverlauf auf Ihrer Profilseite sehen."
|
||||
like: Gefällt mir
|
||||
loading_results: "Lädt Ergebnisse"
|
||||
marketing: "Dies scheint Werbung zu sein"
|
||||
moderate_this_stream: "Diesen Kommentarbereich moderieren"
|
||||
flags:
|
||||
reasons:
|
||||
user:
|
||||
username_offensive: "Unangemessen"
|
||||
username_nolike: "Unerwünscht"
|
||||
username_impersonating: "Identität unklar"
|
||||
username_spam: "Spam"
|
||||
username_other: "Anderes"
|
||||
comment:
|
||||
comment_offensive: "Unangemessen"
|
||||
comment_spam: "Spam"
|
||||
comment_noagree: "Andere Meinung"
|
||||
comment_other: "Anderes"
|
||||
suspect_word: "Verdächtiges Wort"
|
||||
banned_word: "Unzulässiges Wort"
|
||||
body_count: "Text überschreitet Zeichenlimit"
|
||||
trust: "Vertrauen"
|
||||
links: "Link"
|
||||
modqueue:
|
||||
account: "Konto-Markierungen"
|
||||
actions: Aktionen
|
||||
all: Alle
|
||||
all_streams: "Alle Kommentarbereiche"
|
||||
notify_edited: '{0} bearbeitete Kommentar "{1}"'
|
||||
notify_accepted: '{0} genehmigte Kommentar "{1}"'
|
||||
notify_rejected: '{0} lehnte Kommentar "{1}" ab'
|
||||
notify_flagged: '{0} markierte Kommentar "{1}"'
|
||||
notify_reset: '{0} setzte Status von "{1}" zurück'
|
||||
approve: "Freigeben"
|
||||
approved: "Freigegeben"
|
||||
ban_user: "Sperren"
|
||||
billion: Mrd
|
||||
close: Schließen
|
||||
empty_queue: "Keine weiteren Kommentare zu moderieren! Arbeite nicht zu viel, gönn’ dir eine Pause!"
|
||||
flagged: markiert
|
||||
reported: gemeldet
|
||||
less_detail: "Weniger Details"
|
||||
likes: Gefällt mir
|
||||
million: Mio
|
||||
mod_faster: "Schneller moderieren mit Tastaturkürzeln"
|
||||
moderate: "Moderieren →"
|
||||
more_detail: "Mehr Details"
|
||||
new: Neu
|
||||
newest_first: "Neueste zuerst"
|
||||
navigation: Navigation
|
||||
next_comment: "Gehe zum nächsten Kommentar"
|
||||
toggle_search: "Suche öffnen"
|
||||
next_queue: "Nächste Liste"
|
||||
oldest_first: "Älteste zuerst"
|
||||
premod: Unmoderiert
|
||||
prev_comment: " Gehe zum vorherigen Kommentar"
|
||||
reject: "Ablehnen"
|
||||
rejected: "Abgelehnt"
|
||||
reply: "Antworten"
|
||||
select_stream: "Kommentarbereich wählen"
|
||||
shift_key: "⇧"
|
||||
shortcuts: "Tastaturkürzel"
|
||||
sort: "Sortieren"
|
||||
show_shortcuts: "Tastaturkürzel anzeigen"
|
||||
singleview: "Zen-Modus"
|
||||
thismenu: "Dieses Menü öffnen"
|
||||
jump_to_queue: "Zu bestimmter Liste springen"
|
||||
thousand: T
|
||||
try_these: "Versuch’ diese"
|
||||
view_more_shortcuts: "Weitere Tastaturkürzel anzeigen"
|
||||
my_comment_history: "Mein Kommentarverlauf"
|
||||
name: Name
|
||||
no_agree_comment: "Ich bin mit diesem Kommentar nicht einverstanden"
|
||||
no_like_bio: "Ich mag diese Biographie nicht"
|
||||
no_like_username: "Ich mag diesen Nutzernamen nicht"
|
||||
already_flagged_username: "Sie haben diesen Nutzernamen schon markiert."
|
||||
other: Sonstiges
|
||||
permalink: Teilen
|
||||
personal_info: "Dieser Kommentar enthält zu viel personenbezogene Daten"
|
||||
post: Post # Kontext?
|
||||
profile: Profil
|
||||
profile_settings: "Profil-Einstellungen"
|
||||
reply: Antworten
|
||||
report: Melden
|
||||
report_notif: "Vielen Dank für Ihre Meldung. Unsere Moderatoren wurden informiert und werden sich in Kürze darum kümmern."
|
||||
report_notif_remove: "Ihre Meldung wurde entfernt."
|
||||
reported: Gemeldet
|
||||
settings:
|
||||
from_settings_page: "Sie können auf Ihrer Profilseite Ihren Kommentarverlauf einsehen."
|
||||
my_comment_history: "Mein Kommentarverlauf"
|
||||
profile: Profil
|
||||
profile_settings: "Profil-Einstellungen"
|
||||
sign_in: "Anmelden"
|
||||
to_access: "um auf Profil zuzugreifen"
|
||||
user_no_comment: "Keine Kommentare vorhanden. Nehmen Sie an der Diskussion teil!"
|
||||
stream:
|
||||
all_comments: "Alle Kommentare"
|
||||
temporarily_suspended: "Entsprechend der Community-Regeln von {0} wurde Ihr Konto vorübergehend gesperrt. Nehmen Sie {1} wieder an der Diskussion teil."
|
||||
comment_not_found: "Kommentar nicht gefunden"
|
||||
no_comments: "Es gibt noch keine Kommentare. Schreiben Sie doch einen..."
|
||||
no_comments_and_closed: "Es gab zu diesem Artikel keine Kommentare."
|
||||
step_1_header: "Ein Problem melden"
|
||||
step_2_header: "Begründung"
|
||||
step_3_header: "Danke für Ihren Hinweis"
|
||||
streams:
|
||||
all: Alle
|
||||
article: Artikel
|
||||
closed: Geschlossen
|
||||
empty_result: "Ihre Suche war ohne Ergebnisse. Versuchen Sie, Ihre Anfrage weiter zu fassen."
|
||||
filter_streams: "Kommentarbereiche filtern"
|
||||
newest: Neueste
|
||||
oldest: Älteste
|
||||
open: Offen
|
||||
pubdate: "Veröffentlichungsdatum"
|
||||
search: Suchen
|
||||
sort_by: "Sortieren nach"
|
||||
status: "Status"
|
||||
stream_status: "Status"
|
||||
suspenduser:
|
||||
title_suspend: "Nutzer vorübergehend sperren"
|
||||
description_suspend: "Sie sperren vorübergehend {0}. Dieser Kommentar wird auf die Abgelehnt-Liste verschoben und der Nutzer wird keine Aktionen ausführen können, bis die Sperrung abgelaufen ist."
|
||||
select_duration: "Sperrdauer festlegen"
|
||||
one_hour: "1 Stunde"
|
||||
hours: "{0} Stunden"
|
||||
days: "{0} Tage"
|
||||
cancel: "Abbrechen"
|
||||
suspend_user: "Nutzer vorübergehend sperren"
|
||||
email_message_suspend: "Sehr geehrte/r {0}, entsprechend der Community-Richtlinien von {1} wurde Ihr Konto vorübergehend gesperrt. Während der Sperrung können Sie weder kommentieren noch andere Aktionen ausführen. Nehmen Sie {2} wieder an der Diskussion teil."
|
||||
title_notify: "Den Nutzer über die vorübergehende Kontosperrung informieren"
|
||||
notify_suspend_until: "Nutzer {0} wurde vorübergehend gesperrt. Diese Sperrung endet automatisch {1}."
|
||||
description_notify: "Während der Sperrung hat der Nutzer keinen Zugriff auf das Konto."
|
||||
write_message: "Nachricht verfassen"
|
||||
send: Senden
|
||||
reject_username:
|
||||
username: Nutzername
|
||||
no_cancel: "Nein, abbrechen"
|
||||
description_reject: "Möchten Sie diesen Nutzer vorübergehend sperren (Grund: {0})? Der Nutzer kann das Konto erst wieder nach Änderung des {0} nutzen."
|
||||
title_notify: "Nutzer über die vorübergehende Sperrung benachrichtigen"
|
||||
description_notify: "Die Sperrung wird das Nutzerkonto vorübergehend deaktivieren."
|
||||
title_reject: "Uns ist aufgefallen, dass Sie einen Nutzernamen abgelehnt haben"
|
||||
suspend_user: "Nutzer vorübergehend sperren"
|
||||
yes_suspend: "Ja, vorübergehend sperren"
|
||||
email_message_reject: "Ein Community-Mitglied hat Ihren Nutzernamen zur Überprüfung gemeldet. Er wurde als unangemessen eingestuft und abgelehnt. Sie können Ihr Konto erst wieder voll nutzen, wenn Sie Ihren Namen ändern. Setzen Sie sich bei weiteren Fragen mit uns in Verbindung."
|
||||
write_message: "Nachricht schreiben"
|
||||
send: Senden
|
||||
thank_you: "Vielen Dank für Ihre Nachricht. Wir nehmen Ihre Meldung ernst und ein Moderator wird sie umgehend prüfen."
|
||||
user:
|
||||
bio_flags: "Markierungen für diese Biographie"
|
||||
user_bio: "Nutzer-Biographie"
|
||||
username_flags: "Markierungen für diesen Nutzernamen"
|
||||
user_detail:
|
||||
remove_suspension: "Vorübergehende Sperrung aufheben"
|
||||
suspend: "Nutzer vorübergehend sperren"
|
||||
remove_ban: "Sperre aufheben"
|
||||
ban: "Nutzer sperren"
|
||||
member_since: "Mitglied seit"
|
||||
email: "E-Mail"
|
||||
total_comments: "Anzahl Kommentare"
|
||||
reject_rate: "Ablehn-Rate"
|
||||
reports: "Meldungen"
|
||||
all: "Alle"
|
||||
rejected: "Abgelehnte"
|
||||
account_history: "Konto-Verlauf"
|
||||
user_impersonating: "Gibt sich für jemand anderen aus"
|
||||
user_no_comment: "Sie haben noch keinen Kommentar abgegeben. Teilen Sie Ihre Meinung mit uns!"
|
||||
username_offensive: "Dieser Nutzername ist unangemessen"
|
||||
view_conversation: "Diskussion ansehen"
|
||||
install:
|
||||
initial:
|
||||
description: "Erstellen Sie in wenigen Schritten Ihre Talk-Community."
|
||||
submit: "Los geht’s!"
|
||||
add_organization:
|
||||
description: "Geben Sie den Namen Ihrer Organisation ein. Dieser erscheint in E-Mails, wenn Sie neue Mitglieder einladen."
|
||||
label: "Name der Organisation"
|
||||
save: "Speichern"
|
||||
create:
|
||||
email: "E-Mail-Adresse"
|
||||
username: "Nutzername"
|
||||
password: "Passwort"
|
||||
confirm_password: "Passwort bestätigen"
|
||||
save: "Speichern"
|
||||
permitted_domains:
|
||||
title: "Zugelassene Domains"
|
||||
description: "Geben Sie Domains an, für die diese Talk-Instanz freigegeben werden soll. Beispiel: localhost:3000 staging.domain.com domain.com."
|
||||
submit: "Installation abschließen"
|
||||
final:
|
||||
description: "Danke, dass Sie Talk benutzen! Bitte bestätigen Sie Ihre E-Mail-Adresse über den Link, den wir an Sie versandt haben. Während Sie die Einrichtung Ihres Kontos abschließen, können Sie sich schon mit Ihren Lesern beschäftigen."
|
||||
launch: "Talk starten"
|
||||
close: "Schließen"
|
||||
admin_sidebar:
|
||||
view_options: "Ansichtsoptionen"
|
||||
sort_comments: "Kommentare sortieren"
|
||||
+20
-1
@@ -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,8 @@ 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)"
|
||||
flag_username: "Report username"
|
||||
@@ -366,7 +369,7 @@ en:
|
||||
stream:
|
||||
all_comments: "All Comments"
|
||||
temporarily_suspended: "In accordance with {0}'s community guidelines, your account has been temporarily suspended. Please rejoin the conversation {1}."
|
||||
comment_not_found: "Comment was not found"
|
||||
comment_not_found: "This comment has been removed or does not exist."
|
||||
no_comments: "There are no comments yet, why don’t you write one?"
|
||||
no_comments_and_closed: "There were no comments on this article."
|
||||
step_1_header: "Report an issue"
|
||||
@@ -418,6 +421,19 @@ en:
|
||||
bio_flags: "flags for this bio"
|
||||
user_bio: "User Bio"
|
||||
username_flags: "flags for this username"
|
||||
user_detail:
|
||||
remove_suspension: "Remove Suspension"
|
||||
suspend: "Suspend User"
|
||||
remove_ban: "Remove Ban"
|
||||
ban: "Ban User"
|
||||
member_since: "Member Since"
|
||||
email: "Email"
|
||||
total_comments: "Total Comments"
|
||||
reject_rate: "Reject Rate"
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
account_history: "Account History"
|
||||
user_impersonating: "This user is impersonating"
|
||||
user_no_comment: "You've never left a comment. Join the conversation!"
|
||||
username_offensive: "This username is offensive"
|
||||
@@ -444,3 +460,6 @@ en:
|
||||
description: "Thanks for installing Talk! We sent an email to verify your email address. While you finish setting up the account, you can start engaging with your readers now."
|
||||
launch: "Launch Talk"
|
||||
close: "Close this Installer"
|
||||
admin_sidebar:
|
||||
view_options: "View Options"
|
||||
sort_comments: "Sort Comments"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user