replaced eslint:recommended with prettier

This commit is contained in:
Wyatt Johnson
2018-01-11 20:00:34 -07:00
parent d56c19016a
commit 0abc2ca243
649 changed files with 16235 additions and 13008 deletions
@@ -1,13 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import { compose } from 'react-apollo';
import { bindActionCreators } from 'redux';
import errorMsj from 'coral-framework/helpers/error';
import validate from 'coral-framework/helpers/validate';
import CreateUsernameDialog from './CreateUsernameDialog';
import {withSetUsername} from 'coral-framework/graphql/mutations';
import {forEachError} from 'plugin-api/beta/client/utils';
import { withSetUsername } from 'coral-framework/graphql/mutations';
import { forEachError } from 'plugin-api/beta/client/utils';
import t from 'coral-framework/services/i18n';
@@ -25,32 +25,36 @@ class ChangeUsernameContainer extends React.Component {
this.state = {
formData: {
username: (props.auth.user && props.auth.user.username) || ''
username: (props.auth.user && props.auth.user.username) || '',
},
errors: {},
showErrors: false
showErrors: false,
};
}
componentWillReceiveProps(next) {
if (!this.props.auth.showCreateUsernameDialog && next.auth.showCreateUsernameDialog) {
if (
!this.props.auth.showCreateUsernameDialog &&
next.auth.showCreateUsernameDialog
) {
this.setState({
formData: {
username: (this.props.auth.user && this.props.auth.user.username) || '',
username:
(this.props.auth.user && this.props.auth.user.username) || '',
},
});
}
}
handleChange = (e) => {
const {name, value} = e.target;
handleChange = e => {
const { name, value } = e.target;
this.setState(
(state) => ({
state => ({
...state,
formData: {
...state.formData,
[name]: value
}
[name]: value,
},
}),
() => {
this.validation(name, value);
@@ -59,16 +63,16 @@ class ChangeUsernameContainer extends React.Component {
};
addError = (name, error) => {
return this.setState((state) => ({
return this.setState(state => ({
errors: {
...state.errors,
[name]: error
}
[name]: error,
},
}));
};
validation = (name, value) => {
const {addError} = this;
const { addError } = this;
if (!value.length) {
addError(name, t('createdisplay.required_field'));
@@ -77,23 +81,28 @@ class ChangeUsernameContainer extends React.Component {
} else {
const {[name]: prop, ...errors} = this.state.errors; // eslint-disable-line
// Removes Error
this.setState((state) => ({...state, errors}));
this.setState(state => ({ ...state, errors }));
}
};
isCompleted = () => {
const {formData} = this.state;
return !Object.keys(formData).filter((prop) => !formData[prop].length).length;
const { formData } = this.state;
return !Object.keys(formData).filter(prop => !formData[prop].length).length;
};
displayErrors = (show = true) => {
this.setState({showErrors: show});
this.setState({ showErrors: show });
};
async setUsernameAndClose(username, props = this.props) {
const {validForm, invalidForm, setUsername, hideCreateUsernameDialog, updateUsername} = props;
const {
validForm,
invalidForm,
setUsername,
hideCreateUsernameDialog,
updateUsername,
} = props;
try {
// Perform mutation
await setUsername(this.props.auth.user.id, username);
@@ -102,18 +111,17 @@ class ChangeUsernameContainer extends React.Component {
hideCreateUsernameDialog();
validForm();
}
catch(error) {
} catch (error) {
const msgs = [];
forEachError(error, ({msg}) => msgs.push(msg));
forEachError(error, ({ msg }) => msgs.push(msg));
invalidForm(t(msgs.join(', ')));
}
}
handleSubmitUsername = (e) => {
handleSubmitUsername = e => {
e.preventDefault();
const {errors, formData: {username}} = this.state;
const {invalidForm} = this.props;
const { errors, formData: { username } } = this.state;
const { invalidForm } = this.props;
this.displayErrors();
if (this.isCompleted() && !Object.keys(errors).length) {
this.setUsernameAndClose(username);
@@ -127,7 +135,7 @@ class ChangeUsernameContainer extends React.Component {
};
render() {
const {loggedIn, auth} = this.props;
const { loggedIn, auth } = this.props;
return (
<div>
<CreateUsernameDialog
@@ -153,11 +161,11 @@ ChangeUsernameContainer.propTypes = {
changeUsername: PropTypes.func,
};
const mapStateToProps = ({auth}) => ({
auth: auth
const mapStateToProps = ({ auth }) => ({
auth: auth,
});
const mapDispatchToProps = (dispatch) =>
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
showCreateUsernameDialog,
@@ -1,8 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './styles.css';
import {Dialog, Alert, TextField, Button} from 'plugin-api/beta/client/components/ui';
import {FakeComment} from './FakeComment';
import {
Dialog,
Alert,
TextField,
Button,
} from 'plugin-api/beta/client/components/ui';
import { FakeComment } from './FakeComment';
import t from 'coral-framework/services/i18n';
const CreateUsernameDialog = ({
@@ -18,12 +23,12 @@ const CreateUsernameDialog = ({
id="createUsernameDialog"
open={open}
>
<span className={styles.close} onClick={handleClose}>×</span>
<span className={styles.close} onClick={handleClose}>
×
</span>
<div>
<div className={styles.header}>
<h1>
{t('createdisplay.write_your_username')}
</h1>
<h1>{t('createdisplay.write_your_username')}</h1>
</div>
<div>
<p className={styles.yourusername}>
@@ -40,14 +45,16 @@ const CreateUsernameDialog = ({
</p>
{props.auth.error && <Alert>{props.auth.error}</Alert>}
<form id="saveUsername" onSubmit={handleSubmitUsername}>
{props.errors.username &&
{props.errors.username && (
<span className={styles.hint}>
{' '}{t('createdisplay.special_characters')}{' '}
</span>}
{' '}
{t('createdisplay.special_characters')}{' '}
</span>
)}
<div className={styles.saveusername}>
<TextField
id="username"
style={{fontSize: 16}}
style={{ fontSize: 16 }}
type="string"
label={t('createdisplay.username')}
value={formData.username}
@@ -1,25 +1,19 @@
import React from 'react';
import t from 'coral-framework/services/i18n';
import {ReplyButton} from 'talk-plugin-replies';
import { ReplyButton } from 'talk-plugin-replies';
import styles from './FakeComment.css';
import {Icon} from 'plugin-api/beta/client/components/ui';
import {CommentTimestamp} from 'plugin-api/beta/client/components';
import { Icon } from 'plugin-api/beta/client/components/ui';
import { CommentTimestamp } from 'plugin-api/beta/client/components';
export const FakeComment = ({username, created_at, body}) => (
export const FakeComment = ({ username, created_at, body }) => (
<div className={styles.root}>
<span className={styles.authorName}>
{username}
</span>
<span className={styles.authorName}>{username}</span>
<CommentTimestamp created_at={created_at} />
<div className={styles.body}>
{body}
</div>
<div className={styles.body}>{body}</div>
<div className={styles.footer}>
<div>
<button className={styles.button}>
<span className={styles.label}>
{t('like')}
</span>
<span className={styles.label}>{t('like')}</span>
<Icon name="thumb_up" className={styles.icon} />
</button>
<ReplyButton
@@ -30,19 +24,14 @@ export const FakeComment = ({username, created_at, body}) => (
</div>
<div>
<button className={styles.button}>
<span className={styles.label}>
{t('permalink')}
</span>
<span className={styles.label}>{t('permalink')}</span>
<Icon name="link" className={styles.icon} />
</button>
<button className={styles.button}>
<span className={styles.label}>
{t('report')}
</span>
<span className={styles.label}>{t('report')}</span>
<Icon name="flag" className={styles.icon} />
</button>
</div>
</div>
</div>
);
@@ -1,26 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './styles.css';
import {Button, TextField} from 'plugin-api/beta/client/components/ui';
import { Button, TextField } from 'plugin-api/beta/client/components/ui';
import t from 'coral-framework/services/i18n';
class ForgotContent extends React.Component {
state = {value: ''};
state = { value: '' };
handleSubmit = (e) => {
handleSubmit = e => {
e.preventDefault();
this.props.fetchForgotPassword(this.state.value);
};
handleChangeEmail = (e) => {
const {value} = e.target;
this.setState({value});
}
handleChangeEmail = e => {
const { value } = e.target;
this.setState({ value });
};
render() {
const {changeView, auth} = this.props;
const {passwordRequestSuccess, passwordRequestFailure} = auth;
const { changeView, auth } = this.props;
const { passwordRequestSuccess, passwordRequestFailure } = auth;
return (
<div>
@@ -31,7 +30,7 @@ class ForgotContent extends React.Component {
<div className={styles.textField}>
<TextField
type="email"
style={{fontSize: 16}}
style={{ fontSize: 16 }}
id="email"
name="email"
label={t('sign_in.email')}
@@ -47,31 +46,25 @@ class ForgotContent extends React.Component {
>
{t('sign_in.recover_password')}
</Button>
{passwordRequestSuccess
? <p className={styles.passwordRequestSuccess}>
{passwordRequestSuccess ? (
<p className={styles.passwordRequestSuccess}>
{passwordRequestSuccess}
</p>
: null}
{passwordRequestFailure
? <p className={styles.passwordRequestFailure}>
) : null}
{passwordRequestFailure ? (
<p className={styles.passwordRequestFailure}>
{passwordRequestFailure}
</p>
: null}
) : null}
</form>
<div className={styles.footer}>
<span>
{t('sign_in.need_an_account')}
{' '}
<a onClick={() => changeView('SIGNUP')}>
{t('sign_in.register')}
</a>
{t('sign_in.need_an_account')}{' '}
<a onClick={() => changeView('SIGNUP')}>{t('sign_in.register')}</a>
</span>
<span>
{t('sign_in.already_have_an_account')}
{' '}
<a onClick={() => changeView('SIGNIN')}>
{t('sign_in.sign_in')}
</a>
{t('sign_in.already_have_an_account')}{' '}
<a onClick={() => changeView('SIGNIN')}>{t('sign_in.sign_in')}</a>
</span>
</div>
</div>
@@ -1,27 +1,38 @@
import React from 'react';
import {Button, Spinner, Success, Alert} from 'plugin-api/beta/client/components/ui';
import {
Button,
Spinner,
Success,
Alert,
} from 'plugin-api/beta/client/components/ui';
import PropTypes from 'prop-types';
import styles from './ResendVerification.css';
import t from 'coral-framework/services/i18n';
class ResendVerification extends React.Component {
render() {
const {resendVerification, error, loading, success, email} = this.props;
const { resendVerification, error, loading, success, email } = this.props;
return (
<div className="talk-resend-verification">
<h1 className={styles.header}>
{t('sign_in.email_verify_cta')}
</h1>
<h1 className={styles.header}>{t('sign_in.email_verify_cta')}</h1>
{error &&
{error && (
<Alert>
{error.translation_key ? t(`error.${error.translation_key}`) : error.toString()}
</Alert>}
{error.translation_key
? t(`error.${error.translation_key}`)
: error.toString()}
</Alert>
)}
<div className={styles.notVerified}>
{t('error.email_not_verified', email)}
</div>
<div>
<Button id="resendConfirmEmail" cStyle="black" onClick={resendVerification} full>
<Button
id="resendConfirmEmail"
cStyle="black"
onClick={resendVerification}
full
>
{t('sign_in.request_new_verify_email')}
</Button>
{loading && <Spinner />}
@@ -1,5 +1,5 @@
import React from 'react';
import {Dialog} from 'plugin-api/beta/client/components/ui';
import { Dialog } from 'plugin-api/beta/client/components/ui';
import styles from './styles.css';
import SignInContent from './SignInContent';
@@ -7,20 +7,25 @@ import SignUpContent from './SignUpContent';
import ForgotContent from './ForgotContent';
import ResendVerification from './ResendVerification';
const SignDialog = ({open, view, resetSignInDialog, ...props}) => (
const SignDialog = ({ open, view, resetSignInDialog, ...props }) => (
<Dialog className={styles.dialog} id="signInDialog" open={open}>
{view !== 'SIGNIN' && <span className={styles.close} onClick={resetSignInDialog}>×</span>}
{view !== 'SIGNIN' && (
<span className={styles.close} onClick={resetSignInDialog}>
×
</span>
)}
{view === 'SIGNIN' && <SignInContent {...props} />}
{view === 'SIGNUP' && <SignUpContent {...props} />}
{view === 'FORGOT' && <ForgotContent {...props} />}
{view === 'RESEND_VERIFICATION' &&
{view === 'RESEND_VERIFICATION' && (
<ResendVerification
resendVerification={props.resendVerification}
error={props.auth.emailVerificationFailure}
success={props.auth.emailVerificationSuccess}
loading={props.auth.emailVerificationLoading}
email={props.auth.email}
/>}
/>
)}
</Dialog>
);
@@ -1,25 +1,25 @@
import React from 'react';
import {Button} from 'plugin-api/beta/client/components/ui';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {showSignInDialog} from 'coral-embed-stream/src/actions/auth';
import { Button } from 'plugin-api/beta/client/components/ui';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { showSignInDialog } from 'coral-embed-stream/src/actions/auth';
import t from 'coral-framework/services/i18n';
const SignInButton = ({loggedIn, showSignInDialog}) => (
const SignInButton = ({ loggedIn, showSignInDialog }) => (
<div className="talk-stream-auth-sign-in-button">
{!loggedIn
? <Button id="coralSignInButton" onClick={showSignInDialog} full>
{!loggedIn ? (
<Button id="coralSignInButton" onClick={showSignInDialog} full>
{t('sign_in.sign_in_to_comment')}
</Button>
: null}
) : null}
</div>
);
const mapStateToProps = ({auth}) => ({
loggedIn: auth.loggedIn
const mapStateToProps = ({ auth }) => ({
loggedIn: auth.loggedIn,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({showSignInDialog}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators({ showSignInDialog }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(SignInButton);
@@ -1,7 +1,7 @@
import React from 'react';
import {connect} from 'react-redux';
import { connect } from 'react-redux';
import SignDialog from './SignDialog';
import {bindActionCreators} from 'redux';
import { bindActionCreators } from 'redux';
import t from 'coral-framework/services/i18n';
import errorMsj from 'coral-framework/helpers/error';
import validate from 'coral-framework/helpers/validate';
@@ -30,49 +30,48 @@ class SignInContainer extends React.Component {
email: '',
username: '',
password: '',
confirmPassword: ''
confirmPassword: '',
},
errors: {},
showErrors: false
showErrors: false,
};
}
componentDidMount() {
window.addEventListener('storage', this.handleAuth);
const {formData} = this.state;
const { formData } = this.state;
const errors = Object.keys(formData).reduce((map, prop) => {
map[prop] = t('sign_in.required_field');
return map;
}, {});
this.setState({errors});
this.setState({ errors });
}
componentWillUnmount() {
window.removeEventListener('storage', this.handleAuth);
}
handleAuth = (e) => {
handleAuth = e => {
// Listening to FB changes
// FB localStorage key is 'auth'
const authCallback = this.props.facebookCallback;
if (e.key === 'auth') {
const {err, data} = JSON.parse(e.newValue);
const { err, data } = JSON.parse(e.newValue);
authCallback(err, data);
}
};
handleChange = (e) => {
const {name, value} = e.target;
handleChange = e => {
const { name, value } = e.target;
this.setState(
(state) => ({
state => ({
...state,
formData: {
...state.formData,
[name]: value
}
[name]: value,
},
}),
() => {
this.validation(name, value);
@@ -81,29 +80,26 @@ class SignInContainer extends React.Component {
};
resendVerification = () => {
this.props
.requestConfirmEmail(this.props.auth.email)
.then(() => {
setTimeout(() => {
// allow success UI to be shown for a second, and then close the modal
this.props.resetSignInDialog();
}, 2500);
});
this.props.requestConfirmEmail(this.props.auth.email).then(() => {
setTimeout(() => {
// allow success UI to be shown for a second, and then close the modal
this.props.resetSignInDialog();
}, 2500);
});
};
addError = (name, error) => {
return this.setState((state) => ({
return this.setState(state => ({
errors: {
...state.errors,
[name]: error
}
[name]: error,
},
}));
};
validation = (name, value) => {
const {addError} = this;
const {formData} = this.state;
const { addError } = this;
const { formData } = this.state;
if (!value.length) {
addError(name, t('sign_in.required_field'));
@@ -117,23 +113,23 @@ class SignInContainer extends React.Component {
} else {
const {[name]: prop, ...errors} = this.state.errors; // eslint-disable-line
// Removes Error
this.setState((state) => ({...state, errors}));
this.setState(state => ({ ...state, errors }));
}
};
isCompleted = () => {
const {formData} = this.state;
return !Object.keys(formData).filter((prop) => !formData[prop].length).length;
const { formData } = this.state;
return !Object.keys(formData).filter(prop => !formData[prop].length).length;
};
displayErrors = (show = true) => {
this.setState({showErrors: show});
this.setState({ showErrors: show });
};
handleSignUp = (e) => {
handleSignUp = e => {
e.preventDefault();
const {errors} = this.state;
const {fetchSignUp, validForm, invalidForm} = this.props;
const { errors } = this.state;
const { fetchSignUp, validForm, invalidForm } = this.props;
this.displayErrors();
if (this.isCompleted() && !Object.keys(errors).length) {
fetchSignUp(this.state.formData);
@@ -143,14 +139,18 @@ class SignInContainer extends React.Component {
}
};
handleSignIn = (e) => {
handleSignIn = e => {
e.preventDefault();
this.props.fetchSignIn(this.state.formData);
};
render() {
const {auth} = this.props;
const {requireEmailConfirmation, emailVerificationLoading, emailVerificationSuccess} = auth;
const { auth } = this.props;
const {
requireEmailConfirmation,
emailVerificationLoading,
emailVerificationSuccess,
} = auth;
return (
<SignDialog
@@ -167,11 +167,11 @@ class SignInContainer extends React.Component {
}
}
const mapStateToProps = (state) => ({
auth: state.auth
const mapStateToProps = state => ({
auth: state.auth,
});
const mapDispatchToProps = (dispatch) =>
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
facebookCallback,
@@ -185,7 +185,7 @@ const mapDispatchToProps = (dispatch) =>
hideSignInDialog,
resetSignInDialog,
invalidForm,
validForm
validForm,
},
dispatch
);
@@ -1,6 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button, TextField, Spinner, Alert} from 'plugin-api/beta/client/components/ui';
import {
Button,
TextField,
Spinner,
Alert,
} from 'plugin-api/beta/client/components/ui';
import styles from './styles.css';
import t from 'coral-framework/services/i18n';
@@ -10,19 +15,20 @@ const SignInContent = ({
changeView,
handleSignIn,
auth,
fetchSignInFacebook
fetchSignInFacebook,
}) => {
return (
<div className="coral-sign-in">
<div className={`${styles.header} header`}>
<h1>
{t('sign_in.sign_in_to_join')}
</h1>
<h1>{t('sign_in.sign_in_to_join')}</h1>
</div>
{auth.error &&
{auth.error && (
<Alert>
{auth.error.translation_key ? t(`error.${auth.error.translation_key}`) : auth.error.toString()}
</Alert>}
{auth.error.translation_key
? t(`error.${auth.error.translation_key}`)
: auth.error.toString()}
</Alert>
)}
<div>
<div className={`${styles.socialConnections} social-connections`}>
<Button cStyle="facebook" onClick={fetchSignInFacebook} full>
@@ -30,9 +36,7 @@ const SignInContent = ({
</Button>
</div>
<div className={styles.separator}>
<h1>
{t('sign_in.or')}
</h1>
<h1>{t('sign_in.or')}</h1>
</div>
<form onSubmit={handleSignIn}>
<TextField
@@ -40,7 +44,7 @@ const SignInContent = ({
type="email"
label={t('sign_in.email')}
value={formData.email}
style={{fontSize: 16}}
style={{ fontSize: 16 }}
onChange={handleChange}
/>
<TextField
@@ -48,12 +52,12 @@ const SignInContent = ({
type="password"
label={t('sign_in.password')}
value={formData.password}
style={{fontSize: 16}}
style={{ fontSize: 16 }}
onChange={handleChange}
/>
<div className={styles.action}>
{!auth.isLoading
? <Button
{!auth.isLoading ? (
<Button
id="coralLogInButton"
type="submit"
cStyle="black"
@@ -62,7 +66,9 @@ const SignInContent = ({
>
{t('sign_in.sign_in')}
</Button>
: <Spinner />}
) : (
<Spinner />
)}
</div>
</form>
</div>
@@ -87,7 +93,7 @@ SignInContent.propTypes = {
auth: PropTypes.shape({
isLoading: PropTypes.bool.isRequired,
error: PropTypes.string,
emailVerificationFailure: PropTypes.bool
emailVerificationFailure: PropTypes.bool,
}).isRequired,
fetchSignInFacebook: PropTypes.func.isRequired,
handleSignIn: PropTypes.func.isRequired,
@@ -1,10 +1,15 @@
import styles from './styles.css';
import React from 'react';
import {Button, TextField, Spinner, Success, Alert} from 'plugin-api/beta/client/components/ui';
import {
Button,
TextField,
Spinner,
Success,
Alert,
} from 'plugin-api/beta/client/components/ui';
import t from 'coral-framework/services/i18n';
class SignUpContent extends React.Component {
componentWillReceiveProps(next) {
if (
!this.props.emailVerificationEnabled &&
@@ -27,19 +32,17 @@ class SignUpContent extends React.Component {
showErrors,
changeView,
handleSignUp,
fetchSignUpFacebook
fetchSignUpFacebook,
} = this.props;
return (
<div>
<div className={styles.header}>
<h1>
{t('sign_in.sign_up')}
</h1>
<h1>{t('sign_in.sign_up')}</h1>
</div>
{auth.error && <Alert>{auth.error}</Alert>}
{!auth.successSignUp &&
{!auth.successSignUp && (
<div>
<div className={styles.socialConnections}>
<Button cStyle="facebook" onClick={fetchSignUpFacebook} full>
@@ -47,9 +50,7 @@ class SignUpContent extends React.Component {
</Button>
</div>
<div className={styles.separator}>
<h1>
{t('sign_in.or')}
</h1>
<h1>{t('sign_in.or')}</h1>
</div>
<form onSubmit={handleSignUp}>
<TextField
@@ -57,7 +58,7 @@ class SignUpContent extends React.Component {
type="email"
label={t('sign_in.email')}
value={formData.email}
style={{fontSize: 16}}
style={{ fontSize: 16 }}
showErrors={showErrors}
errorMsg={errors.email}
onChange={handleChange}
@@ -68,7 +69,7 @@ class SignUpContent extends React.Component {
label={t('sign_in.username')}
value={formData.username}
showErrors={showErrors}
style={{fontSize: 16}}
style={{ fontSize: 16 }}
errorMsg={errors.username}
onChange={handleChange}
/>
@@ -78,21 +79,23 @@ class SignUpContent extends React.Component {
label={t('sign_in.password')}
value={formData.password}
showErrors={showErrors}
style={{fontSize: 16}}
style={{ fontSize: 16 }}
errorMsg={errors.password}
onChange={handleChange}
minLength="8"
/>
{errors.password &&
{errors.password && (
<span className={styles.hint}>
{' '}Password must be at least 8 characters.{' '}
</span>}
{' '}
Password must be at least 8 characters.{' '}
</span>
)}
<TextField
id="confirmPassword"
type="password"
label={t('sign_in.confirm_password')}
value={formData.confirmPassword}
style={{fontSize: 16}}
style={{ fontSize: 16 }}
showErrors={showErrors}
errorMsg={errors.confirmPassword}
onChange={handleChange}
@@ -111,23 +114,24 @@ class SignUpContent extends React.Component {
{auth.isLoading && <Spinner />}
</div>
</form>
</div>}
{auth.successSignUp &&
</div>
)}
{auth.successSignUp && (
<div>
<Success />
{emailVerificationEnabled &&
{emailVerificationEnabled && (
<p>
{t('sign_in.verify_email')}
<br />
<br />
{t('sign_in.verify_email2')}
</p>}
</div>}
</p>
)}
</div>
)}
<div className={styles.footer}>
{t('sign_in.already_have_an_account')} <a
id="coralSignInViewTrigger"
onClick={() => changeView('SIGNIN')}
>
{t('sign_in.already_have_an_account')}{' '}
<a id="coralSignInViewTrigger" onClick={() => changeView('SIGNIN')}>
{t('sign_in.sign_in')}
</a>
</div>
@@ -1,32 +1,34 @@
import React from 'react';
import styles from './styles.css';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import t from 'coral-framework/services/i18n';
import {logout} from 'coral-embed-stream/src/actions/auth';
import { logout } from 'coral-embed-stream/src/actions/auth';
const UserBox = ({loggedIn, user, logout, onShowProfile}) => (
const UserBox = ({ loggedIn, user, logout, onShowProfile }) => (
<div>
{
loggedIn ? (
<div className={`${styles.userBox} talk-stream-auth-userbox`}>
<span className={styles.userBoxLoggedIn}>{t('sign_in.logged_in_as')}</span>
<a onClick={onShowProfile}>{user.username}</a>. {t('sign_in.not_you')}
<a className={`${styles.logout} talk-stream-userbox-logout`} onClick={() => logout()}>
{t('sign_in.logout')}
</a>
</div>
) : null
}
{loggedIn ? (
<div className={`${styles.userBox} talk-stream-auth-userbox`}>
<span className={styles.userBoxLoggedIn}>
{t('sign_in.logged_in_as')}
</span>
<a onClick={onShowProfile}>{user.username}</a>. {t('sign_in.not_you')}
<a
className={`${styles.logout} talk-stream-userbox-logout`}
onClick={() => logout()}
>
{t('sign_in.logout')}
</a>
</div>
) : null}
</div>
);
const mapStateToProps = ({auth}) => ({
const mapStateToProps = ({ auth }) => ({
loggedIn: auth.loggedIn,
user: auth.user
user: auth.user,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({logout}, dispatch);
const mapDispatchToProps = dispatch => bindActionCreators({ logout }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(UserBox);
+2 -3
View File
@@ -8,7 +8,6 @@ export default {
translations,
slots: {
stream: [UserBox, SignInButton, ChangeUserNameContainer],
login: [SignInContainer]
}
login: [SignInContainer],
},
};
-1
View File
@@ -1,2 +1 @@
module.exports = {};
@@ -1,6 +1,11 @@
import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT, OPEN_MENU, CLOSE_MENU} from './constants';
import {
SET_CONTENT_SLOT,
RESET_CONTENT_SLOT,
OPEN_MENU,
CLOSE_MENU,
} from './constants';
export const setContentSlot = (slot) => ({
export const setContentSlot = slot => ({
type: SET_CONTENT_SLOT,
slot,
});
@@ -9,7 +14,7 @@ export const resetContentSlot = () => ({
type: RESET_CONTENT_SLOT,
});
export const openMenu = (id) => ({
export const openMenu = id => ({
type: OPEN_MENU,
id,
});
@@ -1,10 +1,19 @@
import React from 'react';
import Menu from './Menu';
import styles from './AuthorName.css';
import {ClickOutside} from 'plugin-api/beta/client/components';
import { ClickOutside } from 'plugin-api/beta/client/components';
import cn from 'classnames';
export default ({data, root, asset, comment, contentSlot, menuVisible, toggleMenu, hideMenu}) => {
export default ({
data,
root,
asset,
comment,
contentSlot,
menuVisible,
toggleMenu,
hideMenu,
}) => {
return (
<ClickOutside onClickOutside={hideMenu}>
<div className={cn(styles.root, 'talk-plugin-author-menu')}>
@@ -12,11 +21,9 @@ export default ({data, root, asset, comment, contentSlot, menuVisible, toggleMen
className={cn(styles.button, 'talk-plugin-author-menu-button')}
onClick={toggleMenu}
>
<span className={styles.name}>
{comment.user.username}
</span>
<span className={styles.name}>{comment.user.username}</span>
</button>
{menuVisible &&
{menuVisible && (
<Menu
data={data}
root={root}
@@ -24,7 +31,7 @@ export default ({data, root, asset, comment, contentSlot, menuVisible, toggleMen
comment={comment}
contentSlot={contentSlot}
/>
}
)}
</div>
</ClickOutside>
);
@@ -1,16 +1,16 @@
import React from 'react';
import styles from './Menu.css';
import {Slot} from 'plugin-api/beta/client/components';
import { Slot } from 'plugin-api/beta/client/components';
import cn from 'classnames';
export default ({data, root, asset, comment, contentSlot}) => {
export default ({ data, root, asset, comment, contentSlot }) => {
if (contentSlot) {
return (
<div className={cn(styles.menu, 'talk-plugin-author-menu-popup')}>
<Slot
fill={contentSlot}
data={data}
queryData={{asset, root, comment}}
queryData={{ asset, root, comment }}
/>
</div>
);
@@ -22,13 +22,13 @@ export default ({data, root, asset, comment, contentSlot}) => {
className={cn('talk-plugin-author-menu-infos')}
fill={'authorMenuInfos'}
data={data}
queryData={{asset, root, comment}}
queryData={{ asset, root, comment }}
/>
<Slot
className={cn(styles.actions, 'talk-plugin-author-menu-actions')}
fill={'authorMenuActions'}
data={data}
queryData={{asset, root, comment}}
queryData={{ asset, root, comment }}
/>
</div>
);
@@ -4,4 +4,3 @@ export const SET_CONTENT_SLOT = `${prefix}_SET_CONTENT_SLOT`;
export const RESET_CONTENT_SLOT = `${prefix}_RESET_CONTENT_SLOT`;
export const OPEN_MENU = `${prefix}_OPEN_MENU`;
export const CLOSE_MENU = `${prefix}_CLOSE_MENU`;
@@ -1,15 +1,21 @@
import React from 'react';
import {connect, withFragments} from 'plugin-api/beta/client/hocs';
import {bindActionCreators} from 'redux';
import { connect, withFragments } from 'plugin-api/beta/client/hocs';
import { bindActionCreators } from 'redux';
import AuthorName from '../components/AuthorName';
import {setContentSlot, resetContentSlot, openMenu, closeMenu} from '../actions';
import {compose, gql} from 'react-apollo';
import {getSlotFragmentSpreads, getShallowChanges} from 'plugin-api/beta/client/utils';
import {
setContentSlot,
resetContentSlot,
openMenu,
closeMenu,
} from '../actions';
import { compose, gql } from 'react-apollo';
import {
getSlotFragmentSpreads,
getShallowChanges,
} from 'plugin-api/beta/client/utils';
class AuthorNameContainer extends React.Component {
shouldComponentUpdate(nextProps) {
// Specifically handle `showMenuForComment` if it is the only change.
const changes = getShallowChanges(this.props, nextProps);
if (changes.length === 1 && changes[0] === 'showMenuForComment') {
@@ -32,45 +38,47 @@ class AuthorNameContainer extends React.Component {
} else {
this.props.openMenu(this.props.comment.id);
}
}
};
hideMenu = () => {
if (this.props.showMenuForComment === this.props.comment.id) {
this.props.closeMenu();
}
}
};
render() {
return <AuthorName
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
comment={this.props.comment}
contentSlot={this.props.contentSlot}
menuVisible={this.props.showMenuForComment === this.props.comment.id}
toggleMenu={this.toggleMenu}
hideMenu={this.hideMenu}
/>;
return (
<AuthorName
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
comment={this.props.comment}
contentSlot={this.props.contentSlot}
menuVisible={this.props.showMenuForComment === this.props.comment.id}
toggleMenu={this.toggleMenu}
hideMenu={this.hideMenu}
/>
);
}
}
const slots = [
'authorMenuInfos',
'authorMenuActions',
];
const slots = ['authorMenuInfos', 'authorMenuActions'];
const mapStateToProps = ({talkPluginAuthorMenu: state}) => ({
const mapStateToProps = ({ talkPluginAuthorMenu: state }) => ({
contentSlot: state.contentSlot,
showMenuForComment: state.showMenuForComment,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
setContentSlot,
resetContentSlot,
openMenu,
closeMenu,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
setContentSlot,
resetContentSlot,
openMenu,
closeMenu,
},
dispatch
);
const withAuthorNameFragments = withFragments({
root: gql`
@@ -96,7 +104,7 @@ const withAuthorNameFragments = withFragments({
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withAuthorNameFragments,
withAuthorNameFragments
);
export default enhance(AuthorNameContainer);
@@ -5,7 +5,7 @@ import translations from './translations.yml';
export default {
reducer,
slots: {
commentAuthorName: [AuthorName]
commentAuthorName: [AuthorName],
},
translations
translations,
};
@@ -1,4 +1,9 @@
import {SET_CONTENT_SLOT, RESET_CONTENT_SLOT, OPEN_MENU, CLOSE_MENU} from './constants';
import {
SET_CONTENT_SLOT,
RESET_CONTENT_SLOT,
OPEN_MENU,
CLOSE_MENU,
} from './constants';
const initialState = {
contentSlot: null,
@@ -7,28 +12,28 @@ const initialState = {
export default function reducer(state = initialState, action) {
switch (action.type) {
case SET_CONTENT_SLOT:
return {
...state,
contentSlot: action.slot,
};
case RESET_CONTENT_SLOT:
return {
...state,
contentSlot: initialState.contentSlot,
};
case OPEN_MENU:
return {
...state,
showMenuForComment: action.id,
};
case CLOSE_MENU:
return {
...state,
showMenuForComment: null,
contentSlot: null,
};
default :
return state;
case SET_CONTENT_SLOT:
return {
...state,
contentSlot: action.slot,
};
case RESET_CONTENT_SLOT:
return {
...state,
contentSlot: initialState.contentSlot,
};
case OPEN_MENU:
return {
...state,
showMenuForComment: action.id,
};
case CLOSE_MENU:
return {
...state,
showMenuForComment: null,
contentSlot: null,
};
default:
return state;
}
}
@@ -3,22 +3,22 @@ import Linkify from 'react-linkify';
const name = 'talk-plugin-comment-content';
const CommentContent = ({comment}) => {
const CommentContent = ({ comment }) => {
const textbreaks = comment.body.split('\n');
return <span className={`${name}-text`}>
{
textbreaks.map((line, i) => {
return (
<span className={`${name}-text`}>
{textbreaks.map((line, i) => {
return (
<span key={i} className={`${name}-line`}>
<Linkify properties={{target: '_blank'}}>
{line.trim()}
</Linkify>
{i !== textbreaks.length - 1 && <br className={`${name}-linebreak`}/>}
<Linkify properties={{ target: '_blank' }}>{line.trim()}</Linkify>
{i !== textbreaks.length - 1 && (
<br className={`${name}-linebreak`} />
)}
</span>
);
})
}
</span>;
})}
</span>
);
};
export default CommentContent;
@@ -1,10 +1,11 @@
import {gql} from 'react-apollo';
import {withFragments} from 'plugin-api/beta/client/hocs';
import { gql } from 'react-apollo';
import { withFragments } from 'plugin-api/beta/client/hocs';
import CommentContent from '../components/CommentContent';
export default withFragments({
comment: gql`
fragment TalkPluginCommentContent_comment on Comment {
body
}`
}
`,
})(CommentContent);
@@ -2,6 +2,6 @@ import CommentContent from './containers/CommentContent';
export default {
slots: {
commentContent: [CommentContent]
}
commentContent: [CommentContent],
},
};
+37 -25
View File
@@ -3,25 +3,28 @@ const DataLoader = require('dataloader');
const CommentModel = require('../../models/comment');
console.warn('Enabling the talk-plugin-deep-reply-count plugin introduces a signifigant performance impact on larger sites, use with care.');
console.warn(
'Enabling the talk-plugin-deep-reply-count plugin introduces a signifigant performance impact on larger sites, use with care.'
);
// genDeepCommentCount will return the deep comment count for a given parent id.
const genDeepCommentCount = async (context, parent_ids) => {
// Get all the replies to the parent comments.
const replies = await CommentModel
.find({
const replies = await CommentModel.find(
{
parent_id: {
$in: _.uniq(parent_ids),
},
}, {
},
{
id: 1,
reply_count: 1,
parent_id: 1,
});
}
);
// Get all the replies that have comments on them.
const commentedOnReplies = replies.filter(({reply_count}) => {
const commentedOnReplies = replies.filter(({ reply_count }) => {
return reply_count && reply_count > 0;
});
@@ -29,27 +32,34 @@ const genDeepCommentCount = async (context, parent_ids) => {
// And if there were any..
if (commentedOnReplies.length > 0) {
// Load the reply count for each of them.
deepReplyCount = await context.loaders.Comments.getDeepCount.loadMany(_.uniq(commentedOnReplies.map(({id}) => {
return id;
})));
deepReplyCount = await context.loaders.Comments.getDeepCount.loadMany(
_.uniq(
commentedOnReplies.map(({ id }) => {
return id;
})
)
);
}
// Get all the direct replies to the parent comments.
const allDirectReplies = _.groupBy(replies, 'parent_id');
// Collect all the ancestor replies.
const allAncestorReplies = _.groupBy(_.zip(commentedOnReplies, deepReplyCount), ([{parent_id}]) => {
return parent_id;
});
const allAncestorReplies = _.groupBy(
_.zip(commentedOnReplies, deepReplyCount),
([{ parent_id }]) => {
return parent_id;
}
);
// Return the replies in an array matching that of the input parent_ids array.
return parent_ids.map((parent_id) => {
return parent_ids.map(parent_id => {
// Get the direct replies to this comment.
const directReplies = parent_id in allDirectReplies ? allDirectReplies[parent_id] : [];
const ancestorReplies = parent_id in allAncestorReplies ? allAncestorReplies[parent_id] : [];
const directReplies =
parent_id in allDirectReplies ? allDirectReplies[parent_id] : [];
const ancestorReplies =
parent_id in allAncestorReplies ? allAncestorReplies[parent_id] : [];
// Reduce this array.
return ancestorReplies.reduce((acc, [, count]) => {
@@ -66,16 +76,18 @@ module.exports = {
deepReplyCount: Int
}
`,
loaders: (context) => ({
loaders: context => ({
Comments: {
getDeepCount: new DataLoader((parent_ids) => genDeepCommentCount(context, parent_ids)),
}
getDeepCount: new DataLoader(parent_ids =>
genDeepCommentCount(context, parent_ids)
),
},
}),
resolvers: {
Comment: {
deepReplyCount({id}, args, {loaders: {Comments}}) {
deepReplyCount({ id }, args, { loaders: { Comments } }) {
return Comments.getDeepCount.load(id);
}
}
}
},
},
},
};
+1 -1
View File
@@ -3,5 +3,5 @@ const router = require('./server/router');
module.exports = {
passport,
router
router,
};
@@ -1,34 +1,42 @@
const FacebookStrategy = require('passport-facebook').Strategy;
const UsersService = require('services/users');
const {ValidateUserLogin} = require('services/passport');
let {
ROOT_URL
} = require('config');
const { ValidateUserLogin } = require('services/passport');
let { ROOT_URL } = require('config');
if (ROOT_URL[ROOT_URL.length - 1] !== '/') {
ROOT_URL += '/';
}
module.exports = (passport) => {
if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET && process.env.TALK_ROOT_URL) {
passport.use(new FacebookStrategy({
clientID: process.env.TALK_FACEBOOK_APP_ID,
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
callbackURL: `${ROOT_URL}api/v1/auth/facebook/callback`,
passReqToCallback: true,
profileFields: ['id', 'displayName', 'picture.type(large)']
}, async (req, accessToken, refreshToken, profile, done) => {
module.exports = passport => {
if (
process.env.TALK_FACEBOOK_APP_ID &&
process.env.TALK_FACEBOOK_APP_SECRET &&
process.env.TALK_ROOT_URL
) {
passport.use(
new FacebookStrategy(
{
clientID: process.env.TALK_FACEBOOK_APP_ID,
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
callbackURL: `${ROOT_URL}api/v1/auth/facebook/callback`,
passReqToCallback: true,
profileFields: ['id', 'displayName', 'picture.type(large)'],
},
async (req, accessToken, refreshToken, profile, done) => {
let user;
try {
user = await UsersService.findOrCreateExternalUser(profile);
} catch (err) {
return done(err);
}
let user;
try {
user = await UsersService.findOrCreateExternalUser(profile);
} catch (err) {
return done(err);
}
return ValidateUserLogin(profile, user, done);
}));
return ValidateUserLogin(profile, user, done);
}
)
);
} else if (process.env.NODE_ENV !== 'test') {
throw new Error('Facebook cannot be enabled, missing one of TALK_FACEBOOK_APP_ID, TALK_FACEBOOK_APP_SECRET, TALK_ROOT_URL');
throw new Error(
'Facebook cannot be enabled, missing one of TALK_FACEBOOK_APP_ID, TALK_FACEBOOK_APP_SECRET, TALK_ROOT_URL'
);
}
};
@@ -1,20 +1,29 @@
module.exports = (router) => {
const {passport, HandleAuthPopupCallback} = require('services/passport');
module.exports = router => {
const { passport, HandleAuthPopupCallback } = require('services/passport');
/**
* Facebook auth endpoint, this will redirect the user immediatly to facebook
* for authorization.
*/
router.get('/api/v1/auth/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
router.get(
'/api/v1/auth/facebook',
passport.authenticate('facebook', {
display: 'popup',
authType: 'rerequest',
scope: ['public_profile'],
})
);
/**
* Facebook callback endpoint, this will send the user a html page designed to
* send back the user credentials upon sucesfull login.
*/
router.get('/api/v1/auth/facebook/callback', (req, res, next) => {
// Perform the facebook login flow and pass the data back through the opener.
passport.authenticate('facebook', {session: false}, HandleAuthPopupCallback(req, res, next))(req, res, next);
passport.authenticate(
'facebook',
{ session: false },
HandleAuthPopupCallback(req, res, next)
)(req, res, next);
});
};
@@ -1,4 +1,4 @@
import {OPEN_FEATURED_DIALOG, CLOSE_FEATURED_DIALOG} from './constants';
import { OPEN_FEATURED_DIALOG, CLOSE_FEATURED_DIALOG } from './constants';
export const openFeaturedDialog = (comment, asset) => ({
type: OPEN_FEATURED_DIALOG,
@@ -1,24 +1,27 @@
import React from 'react';
import cn from 'classnames';
import styles from './Comment.css';
import {t} from 'plugin-api/beta/client/services';
import {Slot, CommentAuthorName, CommentTimestamp, CommentContent} from 'plugin-api/beta/client/components';
import {Icon} from 'plugin-api/beta/client/components/ui';
import {pluginName} from '../../package.json';
import { t } from 'plugin-api/beta/client/services';
import {
Slot,
CommentAuthorName,
CommentTimestamp,
CommentContent,
} from 'plugin-api/beta/client/components';
import { Icon } from 'plugin-api/beta/client/components/ui';
import { pluginName } from '../../package.json';
import FeaturedButton from '../containers/FeaturedButton';
class Comment extends React.Component {
viewComment = () => {
this.props.viewComment(this.props.comment.id);
}
};
render() {
const {comment, asset, root, data} = this.props;
const queryData = {comment, asset, root};
const { comment, asset, root, data } = this.props;
const queryData = { comment, asset, root };
return (
<div className={cn(styles.root, `${pluginName}-comment`)}>
<Slot
component={'blockquote'}
className={cn(styles.quote, `${pluginName}-comment-body`)}
@@ -29,7 +32,6 @@ class Comment extends React.Component {
/>
<div className={cn(`${pluginName}-comment-username-box`)}>
<Slot
className={cn(styles.username, `${pluginName}-comment-username`)}
fill="commentAuthorName"
@@ -51,8 +53,13 @@ class Comment extends React.Component {
</div>
<footer className={cn(styles.footer, `${pluginName}-comment-footer`)}>
<div className={cn('talk-embed-stream-comment-actions-container-left', styles.reactionsContainer, `${pluginName}-comment-reactions`)}>
<div
className={cn(
'talk-embed-stream-comment-actions-container-left',
styles.reactionsContainer,
`${pluginName}-comment-reactions`
)}
>
<Slot
fill="commentReactions"
root={root}
@@ -69,9 +76,21 @@ class Comment extends React.Component {
asset={asset}
/>
</div>
<div className={cn('talk-embed-stream-comment-actions-container-right', styles.actionsContainer, `${pluginName}-comment-actions`)}>
<button className={cn(styles.goTo, `${pluginName}-comment-go-to`)} onClick={this.viewComment}>
<Icon name="forum" className={styles.repliesIcon} /> {comment.replyCount} | {t('talk-plugin-featured-comments.go_to_conversation')} <Icon name="keyboard_arrow_right" className={styles.goToIcon} />
<div
className={cn(
'talk-embed-stream-comment-actions-container-right',
styles.actionsContainer,
`${pluginName}-comment-actions`
)}
>
<button
className={cn(styles.goTo, `${pluginName}-comment-go-to`)}
onClick={this.viewComment}
>
<Icon name="forum" className={styles.repliesIcon} />{' '}
{comment.replyCount} |{' '}
{t('talk-plugin-featured-comments.go_to_conversation')}{' '}
<Icon name="keyboard_arrow_right" className={styles.goToIcon} />
</button>
</div>
</footer>
@@ -1,25 +1,29 @@
import React from 'react';
import cn from 'classnames';
import styles from './FeaturedButton.css';
import {pluginName} from '../../package.json';
import {can} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import { pluginName } from '../../package.json';
import { can } from 'plugin-api/beta/client/services';
import { Icon } from 'plugin-api/beta/client/components/ui';
const FeaturedButton = (props) => {
const {alreadyTagged, deleteTag, postTag, user} = props;
const FeaturedButton = props => {
const { alreadyTagged, deleteTag, postTag, user } = props;
return can(user, 'MODERATE_COMMENTS') ? (
<button
className={cn([`${pluginName}-tag-button`, styles.button, {[styles.featured] : alreadyTagged}])}
onClick={alreadyTagged ? deleteTag : postTag} >
{alreadyTagged ?
<Icon name="star" className={styles.icon} /> :
className={cn([
`${pluginName}-tag-button`,
styles.button,
{ [styles.featured]: alreadyTagged },
])}
onClick={alreadyTagged ? deleteTag : postTag}
>
{alreadyTagged ? (
<Icon name="star" className={styles.icon} />
) : (
<Icon name="star_border" className={styles.icon} />
}
)}
</button>
) : null ;
) : null;
};
export default FeaturedButton;
@@ -1,13 +1,16 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import { Dialog } from 'coral-ui';
import styles from './FeaturedDialog.css';
import {t} from 'plugin-api/beta/client/services';
import { t } from 'plugin-api/beta/client/services';
import Button from 'coral-ui/components/Button';
const FeaturedDialog = ({showFeaturedDialog, closeFeaturedDialog, postTag}) => {
const FeaturedDialog = ({
showFeaturedDialog,
closeFeaturedDialog,
postTag,
}) => {
const onPerform = async () => {
await postTag();
await closeFeaturedDialog();
@@ -18,9 +21,14 @@ const FeaturedDialog = ({showFeaturedDialog, closeFeaturedDialog, postTag}) => {
className={cn(styles.dialog, 'talk-featured-dialog')}
id="talkFeaturedDialog"
open={showFeaturedDialog}
onCancel={closeFeaturedDialog} >
<span className={styles.close} onClick={closeFeaturedDialog}>×</span>
<h2 className={styles.header}>{t('talk-plugin-featured-comments.feature_comment')}</h2>
onCancel={closeFeaturedDialog}
>
<span className={styles.close} onClick={closeFeaturedDialog}>
×
</span>
<h2 className={styles.header}>
{t('talk-plugin-featured-comments.feature_comment')}
</h2>
<div className={styles.content}>
{t('talk-plugin-featured-comments.are_you_sure')}
</div>
@@ -29,14 +37,16 @@ const FeaturedDialog = ({showFeaturedDialog, closeFeaturedDialog, postTag}) => {
className={cn(styles.cancel, 'talk-featured-dialog-button-cancel')}
cStyle="cancel"
onClick={closeFeaturedDialog}
raised >
raised
>
{t('talk-plugin-featured-comments.cancel')}
</Button>
<Button
<Button
className={cn(styles.perform, 'talk-featured-dialog-button-confirm')}
cStyle="black"
onClick={onPerform}
raised >
raised
>
{t('talk-plugin-featured-comments.yes_feature_comment')}
</Button>
</div>
@@ -1,11 +1,11 @@
import React from 'react';
import styles from './InfoIcon.css';
import cn from 'classnames';
import {Icon} from 'plugin-api/beta/client/components/ui';
import { Icon } from 'plugin-api/beta/client/components/ui';
export default ({hover}) => (
export default ({ hover }) => (
<Icon
name="info_outline"
className={cn(styles.infoIcon, {[styles.on]: hover})}
className={cn(styles.infoIcon, { [styles.on]: hover })}
/>
);
@@ -1,18 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import { Button } from 'coral-ui';
import t from 'coral-framework/services/i18n';
import cn from 'classnames';
class LoadMore extends React.Component {
render () {
const {loadingState, loadMore} = this.props;
render() {
const { loadingState, loadMore } = this.props;
const disabled = loadingState === 'loading';
return (
<div className='talk-load-more'>
<div className="talk-load-more">
<Button
onClick={loadMore}
className={cn('talk-load-more-button', {[`talk-load-more-button-${loadingState}`]: loadingState})}
className={cn('talk-load-more-button', {
[`talk-load-more-button-${loadingState}`]: loadingState,
})}
disabled={disabled}
>
{t('framework.view_more_comments')}
@@ -1,58 +1,62 @@
import React from 'react';
import cn from 'classnames';
import styles from './ModActionButton.css';
import {pluginName} from '../../package.json';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import { pluginName } from '../../package.json';
import { t } from 'plugin-api/beta/client/services';
import { Icon } from 'plugin-api/beta/client/components/ui';
export class ModActionButton extends React.Component {
constructor() {
super();
this.state = {
on: false
on: false,
};
}
handleMouseEnter = (e) => {
handleMouseEnter = e => {
e.preventDefault();
this.setState({
on: true
on: true,
});
}
};
handleMouseLeave = (e) => {
handleMouseLeave = e => {
e.preventDefault();
this.setState({
on: false
on: false,
});
}
};
handleDeleteTag = () => {
this.props.deleteTag();
this.props.closeMenu();
}
};
handlePostTag = () => {
this.props.postTag();
this.props.closeMenu();
}
};
render() {
const {alreadyTagged} = this.props;
const {handleDeleteTag, handlePostTag} = this;
const { alreadyTagged } = this.props;
const { handleDeleteTag, handlePostTag } = this;
return (
<button className={cn(`${pluginName}-tag-button`, styles.button, {[styles.featured] : alreadyTagged})}
<button
className={cn(`${pluginName}-tag-button`, styles.button, {
[styles.featured]: alreadyTagged,
})}
onClick={alreadyTagged ? handleDeleteTag : handlePostTag}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave} >
onMouseLeave={this.handleMouseLeave}
>
{alreadyTagged ? (
<span className={styles.approved}>
<Icon name="star" className={styles.icon} />
{!this.state.on ? t('talk-plugin-featured-comments.featured') : t('talk-plugin-featured-comments.un_feature')}
{!this.state.on
? t('talk-plugin-featured-comments.featured')
: t('talk-plugin-featured-comments.un_feature')}
</span>
) : (
<span>
@@ -60,11 +64,9 @@ export class ModActionButton extends React.Component {
{t('talk-plugin-featured-comments.feature')}
</span>
)}
</button>
);
}
}
export default ModActionButton;
@@ -2,53 +2,60 @@ import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './ModTag.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import { t } from 'plugin-api/beta/client/services';
import { Icon } from 'plugin-api/beta/client/components/ui';
export default class ModTag extends React.Component {
constructor() {
super();
this.state = {
on: false
on: false,
};
}
handleMouseEnter = (e) => {
handleMouseEnter = e => {
e.preventDefault();
this.setState({
on: true
on: true,
});
}
};
handleMouseLeave = (e) => {
handleMouseLeave = e => {
e.preventDefault();
this.setState({
on: false
on: false,
});
}
};
openFeaturedDialog = (comment, asset) => {
this.props.openFeaturedDialog(comment, asset);
}
};
render() {
const {alreadyTagged, deleteTag, comment, asset} = this.props;
const { alreadyTagged, deleteTag, comment, asset } = this.props;
return alreadyTagged ? (
<span className={cn(styles.tag, styles.featured)}
<span
className={cn(styles.tag, styles.featured)}
onClick={deleteTag}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave} >
onMouseLeave={this.handleMouseLeave}
>
<Icon name="star_outline" className={cn(styles.tagIcon)} />
{!this.state.on ? t('talk-plugin-featured-comments.featured') : t('talk-plugin-featured-comments.un_feature')}
{!this.state.on
? t('talk-plugin-featured-comments.featured')
: t('talk-plugin-featured-comments.un_feature')}
</span>
) : (
<span className={cn(styles.tag, {[styles.featured]: alreadyTagged})}
onClick={() => this.openFeaturedDialog(comment, asset)} >
<span
className={cn(styles.tag, { [styles.featured]: alreadyTagged })}
onClick={() => this.openFeaturedDialog(comment, asset)}
>
<Icon name="star_outline" className={cn(styles.tagIcon)} />
{alreadyTagged ? t('talk-plugin-featured-comments.featured') : t('talk-plugin-featured-comments.feature')}
{alreadyTagged
? t('talk-plugin-featured-comments.featured')
: t('talk-plugin-featured-comments.feature')}
</span>
);
}
@@ -1,15 +1,17 @@
import React from 'react';
import {TabCount} from 'plugin-api/beta/client/components/ui';
import { TabCount } from 'plugin-api/beta/client/components/ui';
import InfoIcon from './InfoIcon';
import {t} from 'plugin-api/beta/client/services';
import { t } from 'plugin-api/beta/client/services';
import Tooltip from './Tooltip';
import PropTypes from 'prop-types';
const Tab = ({active, hover, featuredCommentsCount}) => {
const Tab = ({ active, hover, featuredCommentsCount }) => {
return (
<span>
{t('talk-plugin-featured-comments.featured')}
<TabCount active={active} sub>{featuredCommentsCount}</TabCount>
<TabCount active={active} sub>
{featuredCommentsCount}
</TabCount>
<InfoIcon hover={hover} />
{hover && <Tooltip />}
</span>
@@ -1,7 +1,7 @@
import React from 'react';
import Comment from '../containers/Comment';
import LoadMore from './LoadMore';
import {getErrorMessages} from 'plugin-api/beta/client/utils';
import { getErrorMessages } from 'plugin-api/beta/client/utils';
class TabPane extends React.Component {
state = {
@@ -9,36 +9,43 @@ class TabPane extends React.Component {
};
loadMore = () => {
this.setState({loadingState: 'loading'});
this.props.loadMore()
this.setState({ loadingState: 'loading' });
this.props
.loadMore()
.then(() => {
this.setState({loadingState: 'success'});
this.setState({ loadingState: 'success' });
})
.catch((error) => {
this.setState({loadingState: 'error'});
.catch(error => {
this.setState({ loadingState: 'error' });
this.props.notify('error', getErrorMessages(error));
});
}
};
render() {
const {root, data, asset: {featuredComments, ...asset}, viewComment} = this.props;
const {
root,
data,
asset: { featuredComments, ...asset },
viewComment,
} = this.props;
return (
<div>
{featuredComments.nodes.map((comment) =>
{featuredComments.nodes.map(comment => (
<Comment
key={comment.id}
root={root}
data={data}
comment={comment}
asset={asset}
viewComment={viewComment} />
)}
{featuredComments.hasNextPage &&
viewComment={viewComment}
/>
))}
{featuredComments.hasNextPage && (
<LoadMore
loadMore={this.loadMore}
loadingState={this.state.loadingState}
/>
}
)}
</div>
);
}
@@ -2,39 +2,49 @@ import React from 'react';
import cn from 'classnames';
import styles from './Tag.css';
import Tooltip from './Tooltip';
import {t} from 'plugin-api/beta/client/services';
import { t } from 'plugin-api/beta/client/services';
export default class Tag extends React.Component {
constructor() {
super();
this.state = {
tooltip: false
tooltip: false,
};
}
showTooltip = (e) => {
showTooltip = e => {
e.preventDefault();
this.setState({
tooltip: true
tooltip: true,
});
}
};
hideTooltip = (e) => {
hideTooltip = e => {
e.preventDefault();
this.setState({
tooltip: false
tooltip: false,
});
}
};
render() {
const {tooltip} = this.state;
return(
<span className={cn(styles.tagContainer, styles.noSelect)} onMouseEnter={this.showTooltip}
onMouseLeave={this.hideTooltip} onTouchStart={this.showTooltip}
onTouchEnd={this.hideTooltip} >
<span className={cn(styles.tag, styles.noSelect, {[styles.on]: tooltip}, 'talk-stream-comment-featured-tag-label')}>
const { tooltip } = this.state;
return (
<span
className={cn(styles.tagContainer, styles.noSelect)}
onMouseEnter={this.showTooltip}
onMouseLeave={this.hideTooltip}
onTouchStart={this.showTooltip}
onTouchEnd={this.hideTooltip}
>
<span
className={cn(
styles.tag,
styles.noSelect,
{ [styles.on]: tooltip },
'talk-stream-comment-featured-tag-label'
)}
>
{t('talk-plugin-featured-comments.featured')}
</span>
{tooltip && <Tooltip className={styles.tooltip} />}
@@ -1,10 +1,10 @@
import React from 'react';
import cn from 'classnames';
import styles from './Tooltip.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import { t } from 'plugin-api/beta/client/services';
import { Icon } from 'plugin-api/beta/client/components/ui';
export default ({className = ''}) => (
export default ({ className = '' }) => (
<div className={cn(styles.tooltip, className)}>
<Icon name="info_outline" className={styles.icon} />
<h3 className={styles.headline}>
@@ -1,13 +1,9 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import Comment from '../components/Comment';
import {withFragments} from 'plugin-api/beta/client/hocs';
import {getSlotFragmentSpreads} from 'plugin-api/beta/client/utils';
import { withFragments } from 'plugin-api/beta/client/hocs';
import { getSlotFragmentSpreads } from 'plugin-api/beta/client/utils';
const slots = [
'commentReactions',
'commentAuthorName',
'commentTimestamp',
];
const slots = ['commentReactions', 'commentAuthorName', 'commentTimestamp'];
export default withFragments({
root: gql`
@@ -39,5 +35,5 @@ export default withFragments({
}
${getSlotFragmentSpreads(slots, 'comment')}
}
`
`,
})(Comment);
@@ -1,4 +1,4 @@
import FeaturedButton from '../components/FeaturedButton';
import {withTags} from 'plugin-api/beta/client/hocs';
import { withTags } from 'plugin-api/beta/client/hocs';
export default withTags('featured')(FeaturedButton);
@@ -1,23 +1,26 @@
import {compose} from 'react-apollo';
import {bindActionCreators} from 'redux';
import { compose } from 'react-apollo';
import { bindActionCreators } from 'redux';
import FeaturedDialog from '../components/FeaturedDialog';
import {withTags, connect} from 'plugin-api/beta/client/hocs';
import {closeFeaturedDialog} from '../actions';
import { withTags, connect } from 'plugin-api/beta/client/hocs';
import { closeFeaturedDialog } from '../actions';
const mapStateToProps = ({talkPluginFeaturedComments: state}) => ({
const mapStateToProps = ({ talkPluginFeaturedComments: state }) => ({
showFeaturedDialog: state.showFeaturedDialog,
comment: state.comment,
asset: state.asset,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
closeFeaturedDialog,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
closeFeaturedDialog,
},
dispatch
);
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withTags('featured'),
withTags('featured')
);
export default enhance(FeaturedDialog);
@@ -1,17 +1,20 @@
import {compose} from 'react-apollo';
import {bindActionCreators} from 'redux';
import { compose } from 'react-apollo';
import { bindActionCreators } from 'redux';
import ModActionButton from '../components/ModActionButton';
import {withTags, connect} from 'plugin-api/beta/client/hocs';
import {closeMenu} from 'plugins/talk-plugin-moderation-actions/client/actions';
import { withTags, connect } from 'plugin-api/beta/client/hocs';
import { closeMenu } from 'plugins/talk-plugin-moderation-actions/client/actions';
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
closeMenu,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
closeMenu,
},
dispatch
);
const enhance = compose(
withTags('featured'),
connect(null, mapDispatchToProps),
connect(null, mapDispatchToProps)
);
export default enhance(ModActionButton);
@@ -1,13 +1,13 @@
import React from 'react';
import {gql} from 'react-apollo';
import {connect} from 'react-redux';
import { gql } from 'react-apollo';
import { connect } from 'react-redux';
import Comment from 'coral-admin/src/routes/Moderation/containers/Comment';
import {getDefinitionName} from 'coral-framework/utils';
import { getDefinitionName } from 'coral-framework/utils';
import truncate from 'lodash/truncate';
import t from 'coral-framework/services/i18n';
function prepareNotificationText(text) {
return truncate(text, {length: 50}).replace('\n', ' ');
return truncate(text, { length: 50 }).replace('\n', ' ');
}
class ModSubscription extends React.Component {
@@ -20,14 +20,18 @@ class ModSubscription extends React.Component {
variables: {
assetId: this.props.data.variables.asset_id,
},
updateQuery: (prev, {subscriptionData: {data: {commentFeatured: {user, comment}}}}) => {
const notifyText = this.props.user.id === user.id
? ''
: t(
'talk-plugin-featured-comments.notify_featured',
user.username,
prepareNotificationText(comment.body),
);
updateQuery: (
prev,
{ subscriptionData: { data: { commentFeatured: { user, comment } } } }
) => {
const notifyText =
this.props.user.id === user.id
? ''
: t(
'talk-plugin-featured-comments.notify_featured',
user.username,
prepareNotificationText(comment.body)
);
return this.props.handleCommentChange(prev, comment, notifyText);
},
},
@@ -36,23 +40,33 @@ class ModSubscription extends React.Component {
variables: {
assetId: this.props.data.variables.asset_id,
},
updateQuery: (prev, {subscriptionData: {data: {commentUnfeatured: {user, comment}}}}) => {
const notify = this.props.user.id === user.id
? ''
: t(
'talk-plugin-featured-comments.notify_unfeatured',
user.username,
prepareNotificationText(comment.body),
);
updateQuery: (
prev,
{
subscriptionData: {
data: { commentUnfeatured: { user, comment } },
},
}
) => {
const notify =
this.props.user.id === user.id
? ''
: t(
'talk-plugin-featured-comments.notify_unfeatured',
user.username,
prepareNotificationText(comment.body)
);
return this.props.handleCommentChange(prev, comment, notify);
}
},
},
];
this.subscriptions = configs.map((config) => this.props.data.subscribeToMore(config));
this.subscriptions = configs.map(config =>
this.props.data.subscribeToMore(config)
);
}
componentWillUnmount() {
this.subscriptions.forEach((unsubscribe) => unsubscribe());
this.subscriptions.forEach(unsubscribe => unsubscribe());
}
render() {
@@ -98,7 +112,7 @@ const COMMENT_UNFEATURED_SUBSCRIPTION = gql`
${Comment.fragments.comment}
`;
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
user: state.auth.user,
});
@@ -1,15 +1,18 @@
import ModTag from '../components/ModTag';
import {withTags, connect} from 'plugin-api/beta/client/hocs';
import {gql, compose} from 'react-apollo';
import {bindActionCreators} from 'redux';
import {openFeaturedDialog} from '../actions';
import {notify} from 'plugin-api/beta/client/actions/notification';
import { withTags, connect } from 'plugin-api/beta/client/hocs';
import { gql, compose } from 'react-apollo';
import { bindActionCreators } from 'redux';
import { openFeaturedDialog } from '../actions';
import { notify } from 'plugin-api/beta/client/actions/notification';
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
notify,
openFeaturedDialog,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
notify,
openFeaturedDialog,
},
dispatch
);
const fragments = {
comment: gql`
@@ -18,11 +21,11 @@ const fragments = {
username
}
}
`
`,
};
const enhance = compose(
withTags('featured', {fragments}),
connect(null, mapDispatchToProps),
withTags('featured', { fragments }),
connect(null, mapDispatchToProps)
);
export default enhance(ModTag);
@@ -1,17 +1,21 @@
import Tab from '../components/Tab';
import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
import {compose, gql} from 'react-apollo';
import {withProps} from 'recompose';
import { withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
import { compose, gql } from 'react-apollo';
import { withProps } from 'recompose';
const enhance = compose(
withFragments({
asset: gql`
fragment TalkFeaturedComments_Tab_asset on Asset {
featuredCommentsCount: totalCommentCount(tags: ["FEATURED"]) @skip(if: $hasComment)
}`,
featuredCommentsCount: totalCommentCount(tags: ["FEATURED"])
@skip(if: $hasComment)
}
`,
}),
excludeIf((props) => props.asset.featuredCommentsCount === 0),
withProps((props) => ({featuredCommentsCount: props.asset.featuredCommentsCount})),
excludeIf(props => props.asset.featuredCommentsCount === 0),
withProps(props => ({
featuredCommentsCount: props.asset.featuredCommentsCount,
}))
);
export default enhance(Tab);
@@ -1,16 +1,18 @@
import React from 'react';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import TabPane from '../components/TabPane';
import {withFragments, connect} from 'plugin-api/beta/client/hocs';
import { withFragments, connect } from 'plugin-api/beta/client/hocs';
import Comment from '../containers/Comment';
import {notify} from 'plugin-api/beta/client/actions/notification';
import {viewComment} from 'coral-embed-stream/src/actions/stream';
import {appendNewNodes, getDefinitionName} from 'plugin-api/beta/client/utils';
import { notify } from 'plugin-api/beta/client/actions/notification';
import { viewComment } from 'coral-embed-stream/src/actions/stream';
import {
appendNewNodes,
getDefinitionName,
} from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
class TabPaneContainer extends React.Component {
loadMore = () => {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
@@ -22,17 +24,17 @@ class TabPaneContainer extends React.Component {
sortBy: this.props.data.variables.sortBy,
excludeIgnored: this.props.data.variables.excludeIgnored,
},
updateQuery: (previous, {fetchMoreResult:{comments}}) => {
updateQuery: (previous, { fetchMoreResult: { comments } }) => {
const updated = update(previous, {
asset: {
featuredComments: {
nodes: {
$apply: (nodes) => appendNewNodes(nodes, comments.nodes),
$apply: nodes => appendNewNodes(nodes, comments.nodes),
},
hasNextPage: {$set: comments.hasNextPage},
endCursor: {$set: comments.endCursor},
hasNextPage: { $set: comments.hasNextPage },
endCursor: { $set: comments.endCursor },
},
}
},
});
return updated;
},
@@ -40,10 +42,7 @@ class TabPaneContainer extends React.Component {
};
render() {
return <TabPane
{...this.props}
loadMore={this.loadMore}
/>;
return <TabPane {...this.props} loadMore={this.loadMore} />;
}
}
@@ -78,11 +77,14 @@ const LOAD_MORE_QUERY = gql`
${Comment.fragments.comment}
`;
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
viewComment,
notify,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
viewComment,
notify,
},
dispatch
);
const enhance = compose(
connect(null, mapDispatchToProps),
@@ -118,7 +120,7 @@ const enhance = compose(
${Comment.fragments.comment}
${Comment.fragments.asset}
`,
}),
})
);
export default enhance(TabPaneContainer);
@@ -1,7 +1,7 @@
import {compose, gql} from 'react-apollo';
import { compose, gql } from 'react-apollo';
import Tag from '../components/Tag';
import {isTagged} from 'plugin-api/beta/client/utils';
import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
import { isTagged } from 'plugin-api/beta/client/utils';
import { withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
export default compose(
withFragments({
@@ -13,6 +13,7 @@ export default compose(
}
}
}
`}),
excludeIf((props) => !isTagged(props.comment.tags, 'FEATURED'))
`,
}),
excludeIf(props => !isTagged(props.comment.tags, 'FEATURED'))
)(Tag);
@@ -7,11 +7,11 @@ import ModTag from './containers/ModTag';
import ModActionButton from './containers/ModActionButton';
import ModSubscription from './containers/ModSubscription';
import FeaturedDialog from './containers/FeaturedDialog';
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import reducer from './reducer';
import {findCommentInEmbedQuery} from 'coral-embed-stream/src/graphql/utils';
import {prependNewNodes} from 'plugin-api/beta/client/utils';
import { findCommentInEmbedQuery } from 'coral-embed-stream/src/graphql/utils';
import { prependNewNodes } from 'plugin-api/beta/client/utils';
export default {
translations,
@@ -25,32 +25,35 @@ export default {
adminCommentInfoBar: [ModTag],
},
mutations: {
IgnoreUser: ({variables}) => ({
IgnoreUser: ({ variables }) => ({
updateQueries: {
CoralEmbedStream_Embed: (previous) => {
CoralEmbedStream_Embed: previous => {
if (!previous.asset.featuredComments) {
return previous;
}
const ignoredUserId = variables.id;
const newNodes = previous.asset.featuredComments.nodes.filter((n) => n.user.id !== ignoredUserId);
const removedCount = previous.asset.featuredComments.nodes.length - newNodes.length;
const newNodes = previous.asset.featuredComments.nodes.filter(
n => n.user.id !== ignoredUserId
);
const removedCount =
previous.asset.featuredComments.nodes.length - newNodes.length;
const updated = update(previous, {
asset: {
featuredComments: {
nodes: {$set: newNodes}
nodes: { $set: newNodes },
},
featuredCommentsCount: {
$apply: (value) => value - removedCount,
}
}
$apply: value => value - removedCount,
},
},
});
return updated;
}
}
},
},
}),
AddTag: ({variables}) => ({
AddTag: ({ variables }) => ({
updateQueries: {
CoralEmbedStream_Embed: (previous) => {
CoralEmbedStream_Embed: previous => {
let updated = previous;
if (variables.name !== 'FEATURED') {
@@ -64,21 +67,20 @@ export default {
asset: {
featuredComments: {
nodes: {
$apply: (nodes) => prependNewNodes(nodes, [comment]),
}
$apply: nodes => prependNewNodes(nodes, [comment]),
},
},
featuredCommentsCount: {
$apply: (value) => value + 1
$apply: value => value + 1,
},
}
},
});
}
return updated;
}
},
},
update: (proxy) => {
update: proxy => {
if (variables.name !== 'FEATURED') {
return;
}
@@ -91,16 +93,16 @@ export default {
}
`;
const data = proxy.readFragment({fragment, id: fragmentId});
const data = proxy.readFragment({ fragment, id: fragmentId });
data.status = 'ACCEPTED';
proxy.writeFragment({fragment, id: fragmentId, data});
proxy.writeFragment({ fragment, id: fragmentId, data });
},
}),
RemoveTag: ({variables}) => ({
RemoveTag: ({ variables }) => ({
updateQueries: {
CoralEmbedStream_Embed: (previous) => {
CoralEmbedStream_Embed: previous => {
let updated = previous;
if (variables.name !== 'FEATURED') {
@@ -112,20 +114,19 @@ export default {
asset: {
featuredComments: {
nodes: {
$apply: (nodes) =>
nodes.filter((n) => n.id !== variables.id)
}
$apply: nodes => nodes.filter(n => n.id !== variables.id),
},
},
featuredCommentsCount: {
$apply: (value) => value - 1
}
}
$apply: value => value - 1,
},
},
});
}
return updated;
},
}
})
},
}),
},
};
@@ -1,10 +1,10 @@
import {OPEN_FEATURED_DIALOG, CLOSE_FEATURED_DIALOG} from './constants';
import { OPEN_FEATURED_DIALOG, CLOSE_FEATURED_DIALOG } from './constants';
const initialState = {
showFeaturedDialog: false,
comment: {
id: null,
tags: []
tags: [],
},
asset: {
id: null,
@@ -13,20 +13,20 @@ const initialState = {
export default function reducer(state = initialState, action) {
switch (action.type) {
case OPEN_FEATURED_DIALOG:
return {
...state,
comment: action.comment,
asset: action.asset,
showFeaturedDialog: true,
};
case CLOSE_FEATURED_DIALOG:
return {
...state,
featuredCommentId: null,
showFeaturedDialog: false,
};
default :
return state;
case OPEN_FEATURED_DIALOG:
return {
...state,
comment: action.comment,
asset: action.asset,
showFeaturedDialog: true,
};
case CLOSE_FEATURED_DIALOG:
return {
...state,
featuredCommentId: null,
showFeaturedDialog: false,
};
default:
return state;
}
}
+27 -16
View File
@@ -1,4 +1,4 @@
const {check} = require('perms/utils');
const { check } = require('perms/utils');
module.exports = {
typeDefs: `
@@ -24,18 +24,18 @@ module.exports = {
`,
resolvers: {
Subscription: {
commentFeatured: ({user, comment}) => {
return {user, comment};
commentFeatured: ({ user, comment }) => {
return { user, comment };
},
commentUnfeatured: ({user, comment}) => {
return {user, comment};
commentUnfeatured: ({ user, comment }) => {
return { user, comment };
},
},
},
setupFunctions: {
commentFeatured: (options, args) => ({
commentFeatured: {
filter: ({comment}, {user}) => {
filter: ({ comment }, { user }) => {
if (args.asset_id === null) {
return check(user, ['ADMIN', 'MODERATOR']);
}
@@ -45,7 +45,7 @@ module.exports = {
}),
commentUnfeatured: (options, args) => ({
commentUnfeatured: {
filter: ({comment}, {user}) => {
filter: ({ comment }, { user }) => {
if (args.asset_id === null) {
return check(user, ['ADMIN', 'MODERATOR']);
}
@@ -57,21 +57,32 @@ module.exports = {
hooks: {
RootMutation: {
addTag: {
async post(obj, {tag: {name, id, item_type}}, {user, mutators: {Comment}, pubsub}) {
async post(
obj,
{ tag: { name, id, item_type } },
{ user, mutators: { Comment }, pubsub }
) {
if (name === 'FEATURED' && item_type === 'COMMENTS') {
const comment = await Comment.setStatus({id: id, status: 'ACCEPTED'});
const comment = await Comment.setStatus({
id: id,
status: 'ACCEPTED',
});
if (comment) {
pubsub.publish('commentFeatured', {comment, user});
pubsub.publish('commentFeatured', { comment, user });
}
}
},
},
removeTag: {
async post(obj, {tag: {name, id, item_type}}, {user, loaders: {Comments}, pubsub}) {
async post(
obj,
{ tag: { name, id, item_type } },
{ user, loaders: { Comments }, pubsub }
) {
if (name === 'FEATURED' && item_type === 'COMMENTS') {
const comment = await Comments.get.load(id);
if (comment) {
pubsub.publish('commentUnfeatured', {comment, user});
pubsub.publish('commentUnfeatured', { comment, user });
}
}
},
@@ -84,10 +95,10 @@ module.exports = {
permissions: {
public: true,
self: false,
roles: ['ADMIN', 'MODERATOR']
roles: ['ADMIN', 'MODERATOR'],
},
models: ['COMMENTS'],
created_at: new Date()
}
]
created_at: new Date(),
},
],
};
@@ -1,18 +1,22 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './FlagDetails.css';
import {t} from 'plugin-api/beta/client/services';
import {Slot, IfSlotIsNotEmpty, CommentDetail} from 'plugin-api/beta/client/components';
import { t } from 'plugin-api/beta/client/services';
import {
Slot,
IfSlotIsNotEmpty,
CommentDetail,
} from 'plugin-api/beta/client/components';
class FlagDetails extends Component {
render() {
const {comment: {actions}, more, data, root, comment} = this.props;
const { comment: { actions }, more, data, root, comment } = this.props;
const flagActions = actions && actions.filter((a) => a.__typename === 'FlagAction');
const flagActions =
actions && actions.filter(a => a.__typename === 'FlagAction');
const summaries = flagActions.reduce((sum, action) => {
if (!(action.reason in sum)) {
sum[action.reason] = {count: 0, actions: []};
sum[action.reason] = { count: 0, actions: [] };
}
sum[action.reason].count++;
if (action.user) {
@@ -30,16 +34,21 @@ class FlagDetails extends Component {
return (
<CommentDetail
icon={'flag'}
header={`${t('talk-plugin-flag-details.flags')} (${Object.keys(summaries).length})`}
header={`${t('talk-plugin-flag-details.flags')} (${
Object.keys(summaries).length
})`}
info={
<ul className={styles.info}>
{reasons.map((reason) =>
{reasons.map(reason => (
<li key={reason} className={styles.lessDetail}>
{t(`flags.reasons.comment.${reason.toLowerCase()}`)} {summaries[reason].userFlagged && `(${summaries[reason].count})`}
{t(`flags.reasons.comment.${reason.toLowerCase()}`)}{' '}
{summaries[reason].userFlagged &&
`(${summaries[reason].count})`}
</li>
)}
))}
</ul>
}>
}
>
{more && (
<IfSlotIsNotEmpty
slot="adminCommentMoreFlagDetails"
@@ -62,10 +71,12 @@ FlagDetails.propTypes = {
data: PropTypes.object,
root: PropTypes.object,
comment: PropTypes.shape({
actions: PropTypes.arrayOf(PropTypes.shape({
message: PropTypes.string,
user: PropTypes.shape({username: PropTypes.string})
})).isRequired,
actions: PropTypes.arrayOf(
PropTypes.shape({
message: PropTypes.string,
user: PropTypes.shape({ username: PropTypes.string }),
})
).isRequired,
}).isRequired,
};
@@ -1,20 +1,20 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {t} from 'plugin-api/beta/client/services';
import { t } from 'plugin-api/beta/client/services';
import styles from './UserFlagDetails.css';
class UserFlagDetails extends Component {
render() {
const {comment: {actions}, viewUserDetail} = this.props;
const { comment: { actions }, viewUserDetail } = this.props;
const flagActions = actions && actions.filter((a) => a.__typename === 'FlagAction');
const flagActions =
actions && actions.filter(a => a.__typename === 'FlagAction');
const summaries = flagActions.reduce((sum, action) => {
if (!action.user) {
return sum;
}
if (!(action.reason in sum)) {
sum[action.reason] = {count: 0, actions: []};
sum[action.reason] = { count: 0, actions: [] };
}
sum[action.reason].count++;
sum[action.reason].actions.push(action);
@@ -23,25 +23,28 @@ class UserFlagDetails extends Component {
return (
<ul className={styles.detail}>
{Object.keys(summaries)
.map((reason) => (
<li key={reason}>
{t(`flags.reasons.comment.${reason.toLowerCase()}`)} ({summaries[reason].count})
<ul className={styles.subDetail}>
{summaries[reason].actions.map((action) =>
<li key={action.user.id}>
{action.user &&
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
{action.user.username}
</a>
}
{action.message}
</li>
)}
</ul>
</li>
))
}
{Object.keys(summaries).map(reason => (
<li key={reason}>
{t(`flags.reasons.comment.${reason.toLowerCase()}`)} ({
summaries[reason].count
})
<ul className={styles.subDetail}>
{summaries[reason].actions.map(action => (
<li key={action.user.id}>
{action.user && (
<a
className={styles.username}
onClick={() => viewUserDetail(action.user.id)}
>
{action.user.username}
</a>
)}
{action.message}
</li>
))}
</ul>
</li>
))}
</ul>
);
}
@@ -49,10 +52,12 @@ class UserFlagDetails extends Component {
UserFlagDetails.propTypes = {
comment: PropTypes.shape({
actions: PropTypes.arrayOf(PropTypes.shape({
message: PropTypes.string,
user: PropTypes.shape({username: PropTypes.string})
})).isRequired,
actions: PropTypes.arrayOf(
PropTypes.shape({
message: PropTypes.string,
user: PropTypes.shape({ username: PropTypes.string }),
})
).isRequired,
}).isRequired,
viewUserDetail: PropTypes.func.isRequired,
};
@@ -1,11 +1,9 @@
import {compose, gql} from 'react-apollo';
import { compose, gql } from 'react-apollo';
import FlagDetails from '../components/FlagDetails';
import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
import {getSlotFragmentSpreads} from 'plugin-api/beta/client/utils';
import { withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
import { getSlotFragmentSpreads } from 'plugin-api/beta/client/utils';
const slots = [
'adminCommentMoreFlagDetails',
];
const slots = ['adminCommentMoreFlagDetails'];
const enhance = compose(
withFragments({
@@ -31,9 +29,12 @@ const enhance = compose(
}
${getSlotFragmentSpreads(slots, 'comment')}
}
`
`,
}),
excludeIf(({comment: {actions}}) => !actions.some((action) => action.__typename === 'FlagAction')),
excludeIf(
({ comment: { actions } }) =>
!actions.some(action => action.__typename === 'FlagAction')
)
);
export default enhance(FlagDetails);
@@ -1,14 +1,17 @@
import {compose, gql} from 'react-apollo';
import { compose, gql } from 'react-apollo';
import UserFlagDetails from '../components/UserFlagDetails';
import {bindActionCreators} from 'redux';
import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
import {viewUserDetail} from 'plugin-api/beta/client/actions/admin';
import {connect} from 'react-redux';
import { bindActionCreators } from 'redux';
import { withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
import { viewUserDetail } from 'plugin-api/beta/client/actions/admin';
import { connect } from 'react-redux';
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
viewUserDetail,
}, dispatch)
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
viewUserDetail,
},
dispatch
),
});
const enhance = compose(
@@ -29,11 +32,12 @@ const enhance = compose(
}
}
}
`
`,
}),
excludeIf(({comment: {actions}}) =>
!actions.some((action) => action.__typename === 'FlagAction' && action.user
)),
excludeIf(
({ comment: { actions } }) =>
!actions.some(action => action.__typename === 'FlagAction' && action.user)
)
);
export default enhance(UserFlagDetails);
@@ -7,5 +7,5 @@ export default {
slots: {
adminCommentDetailArea: [FlagDetails],
adminCommentMoreFlagDetails: [UserFlagDetails],
}
},
};
@@ -1,12 +1,13 @@
import React from 'react';
import styles from './IgnoreUserAction.css';
import {t} from 'plugin-api/beta/client/services';
import { t } from 'plugin-api/beta/client/services';
import cn from 'classnames';
export default ({ignoreUser}) => (
export default ({ ignoreUser }) => (
<button
className={cn(styles.button, 'talk-plugin-ignore-user-action')}
onClick={ignoreUser}>
onClick={ignoreUser}
>
{t('talk-plugin-ignore-user.ignore_user')}
</button>
);
@@ -1,9 +1,9 @@
import React from 'react';
import styles from './IgnoreUserConfirmation.css';
import {t} from 'plugin-api/beta/client/services';
import { t } from 'plugin-api/beta/client/services';
import cn from 'classnames';
export default ({ignoreUser, cancel, username}) => (
export default ({ ignoreUser, cancel, username }) => (
<aside className={cn(styles.root, 'talk-plugin-ignore-user-confirmation')}>
<div className={styles.message}>
<h1 className={styles.title}>
@@ -11,11 +11,28 @@ export default ({ignoreUser, cancel, username}) => (
</h1>
{t('talk-plugin-ignore-user.confirmation')}
</div>
<div className={cn(styles.actions, 'talk-plugin-ignore-user-confirmation-actions')}>
<button className={cn(styles.cancel, 'talk-plugin-ignore-user-confirmation-cancel')} onClick={cancel}>
<div
className={cn(
styles.actions,
'talk-plugin-ignore-user-confirmation-actions'
)}
>
<button
className={cn(
styles.cancel,
'talk-plugin-ignore-user-confirmation-cancel'
)}
onClick={cancel}
>
{t('talk-plugin-ignore-user.cancel')}
</button>
<button className={cn(styles.button, 'talk-plugin-ignore-user-confirmation-button')} onClick={ignoreUser}>
<button
className={cn(
styles.button,
'talk-plugin-ignore-user-confirmation-button'
)}
onClick={ignoreUser}
>
{t('talk-plugin-ignore-user.ignore_user')}
</button>
</div>
@@ -1,17 +1,17 @@
import React from 'react';
import styles from './IgnoredUserSection.css';
import {t} from 'plugin-api/beta/client/services';
import { t } from 'plugin-api/beta/client/services';
export default ({ignoredUsers, stopIgnoringUser}) => (
export default ({ ignoredUsers, stopIgnoringUser }) => (
<section className={'talk-plugin-ignore-user-section'}>
<h3>{t('talk-plugin-ignore-user.section_title')}</h3>
<p>{t('talk-plugin-ignore-user.section_info')}</p>
<ul className={styles.list}>
{ignoredUsers.map(({username, id}) => (
{ignoredUsers.map(({ username, id }) => (
<li className={styles.listItem} key={id}>
<span className={styles.username}>{username}</span>
<button
onClick={() => stopIgnoringUser({id})}
onClick={() => stopIgnoringUser({ id })}
className={styles.button}
>
{t('talk-plugin-ignore-user.stop_ignoring')}
@@ -1,29 +1,29 @@
import React from 'react';
import IgnoreUserAction from '../components/IgnoreUserAction';
import {compose, gql} from 'react-apollo';
import {connect, withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
import {bindActionCreators} from 'redux';
import {setContentSlot} from 'plugins/talk-plugin-author-menu/client/actions';
import { compose, gql } from 'react-apollo';
import { connect, withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
import { bindActionCreators } from 'redux';
import { setContentSlot } from 'plugins/talk-plugin-author-menu/client/actions';
import IgnoreUserConfirmation from './IgnoreUserConfirmation';
import {getDefinitionName} from 'plugin-api/beta/client/utils';
import { getDefinitionName } from 'plugin-api/beta/client/utils';
class IgnoreUserActionContainer extends React.Component {
ignoreUser = () => {
this.props.setContentSlot('ignoreUserConfirmation');
};
render() {
return <IgnoreUserAction
ignoreUser={this.ignoreUser}
/>;
return <IgnoreUserAction ignoreUser={this.ignoreUser} />;
}
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
setContentSlot,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
setContentSlot,
},
dispatch
);
const withIgnoreUserActionFragments = withFragments({
root: gql`
@@ -47,7 +47,7 @@ const withIgnoreUserActionFragments = withFragments({
const enhance = compose(
connect(null, mapDispatchToProps),
withIgnoreUserActionFragments,
excludeIf(({root: {me}, comment}) => !me || me.id === comment.user.id),
excludeIf(({ root: { me }, comment }) => !me || me.id === comment.user.id)
);
export default enhance(IgnoreUserActionContainer);
@@ -1,22 +1,28 @@
import React from 'react';
import IgnoreUserConfirmation from '../components/IgnoreUserConfirmation';
import {compose, gql} from 'react-apollo';
import {connect, withFragments, withIgnoreUser} from 'plugin-api/beta/client/hocs';
import {bindActionCreators} from 'redux';
import {closeMenu} from 'plugins/talk-plugin-author-menu/client/actions';
import {notify} from 'plugin-api/beta/client/actions/notification';
import {t} from 'plugin-api/beta/client/services';
import {getErrorMessages} from 'plugin-api/beta/client/utils';
import { compose, gql } from 'react-apollo';
import {
connect,
withFragments,
withIgnoreUser,
} from 'plugin-api/beta/client/hocs';
import { bindActionCreators } from 'redux';
import { closeMenu } from 'plugins/talk-plugin-author-menu/client/actions';
import { notify } from 'plugin-api/beta/client/actions/notification';
import { t } from 'plugin-api/beta/client/services';
import { getErrorMessages } from 'plugin-api/beta/client/utils';
class IgnoreUserConfirmationContainer extends React.Component {
ignoreUser = () => {
const {ignoreUser, notify, comment, closeMenu} = this.props;
const { ignoreUser, notify, comment, closeMenu } = this.props;
ignoreUser(comment.user.id)
.then(() => {
notify('success', t('talk-plugin-ignore-user.notify_success', comment.user.username));
notify(
'success',
t('talk-plugin-ignore-user.notify_success', comment.user.username)
);
})
.catch((err) => {
.catch(err => {
notify('error', getErrorMessages(err));
});
closeMenu();
@@ -24,22 +30,27 @@ class IgnoreUserConfirmationContainer extends React.Component {
cancel = () => {
this.props.closeMenu();
}
};
render() {
return <IgnoreUserConfirmation
username={this.props.comment.user.username}
ignoreUser={this.ignoreUser}
cancel={this.cancel}
/>;
return (
<IgnoreUserConfirmation
username={this.props.comment.user.username}
ignoreUser={this.ignoreUser}
cancel={this.cancel}
/>
);
}
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
closeMenu,
notify,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
closeMenu,
notify,
},
dispatch
);
const withIgnoreUserConfirmationFragments = withFragments({
comment: gql`
@@ -48,13 +59,14 @@ const withIgnoreUserConfirmationFragments = withFragments({
id
username
}
}`,
}
`,
});
const enhance = compose(
connect(null, mapDispatchToProps),
withIgnoreUserConfirmationFragments,
withIgnoreUser,
withIgnoreUser
);
export default enhance(IgnoreUserConfirmationContainer);
@@ -1,15 +1,20 @@
import React from 'react';
import IgnoredUserSection from '../components/IgnoredUserSection';
import {compose, gql} from 'react-apollo';
import {withFragments, excludeIf, withStopIgnoringUser} from 'plugin-api/beta/client/hocs';
import { compose, gql } from 'react-apollo';
import {
withFragments,
excludeIf,
withStopIgnoringUser,
} from 'plugin-api/beta/client/hocs';
class IgnoredUserSectionContainer extends React.Component {
render() {
return <IgnoredUserSection
stopIgnoringUser={this.props.stopIgnoringUser}
ignoredUsers={this.props.root.me.ignoredUsers}
/>;
return (
<IgnoredUserSection
stopIgnoringUser={this.props.stopIgnoringUser}
ignoredUsers={this.props.root.me.ignoredUsers}
/>
);
}
}
@@ -19,8 +24,8 @@ const withIgnoredUserSectionFragments = withFragments({
me {
id
ignoredUsers {
id,
username,
id
username
}
}
}
@@ -30,7 +35,7 @@ const withIgnoredUserSectionFragments = withFragments({
const enhance = compose(
withIgnoredUserSectionFragments,
withStopIgnoringUser,
excludeIf(({root: {me}}) => me.ignoredUsers.length === 0),
excludeIf(({ root: { me } }) => me.ignoredUsers.length === 0)
);
export default enhance(IgnoredUserSectionContainer);
+31 -17
View File
@@ -12,32 +12,46 @@ export default {
},
translations,
mutations: {
IgnoreUser: ({variables}) => ({
IgnoreUser: ({ variables }) => ({
updateQueries: {
CoralEmbedStream_Embed: (previousData) => {
CoralEmbedStream_Embed: previousData => {
const ignoredUserId = variables.id;
const updated = update(previousData, {me: {ignoredUsers: {$push: [{
id: ignoredUserId,
__typename: 'User',
}]}}});
const updated = update(previousData, {
me: {
ignoredUsers: {
$push: [
{
id: ignoredUserId,
__typename: 'User',
},
],
},
},
});
return updated;
}
}
},
},
}),
StopIgnoringUser: ({variables}) => ({
StopIgnoringUser: ({ variables }) => ({
updateQueries: {
CoralEmbedStream_Profile: (previousData) => {
CoralEmbedStream_Profile: previousData => {
const noLongerIgnoredUserId = variables.id;
// remove noLongerIgnoredUserId from ignoredUsers
const updated = update(previousData, {me: {ignoredUsers: {
$apply: (ignoredUsers) => {
return ignoredUsers.filter((u) => u.id !== noLongerIgnoredUserId);
}
}}});
const updated = update(previousData, {
me: {
ignoredUsers: {
$apply: ignoredUsers => {
return ignoredUsers.filter(
u => u.id !== noLongerIgnoredUserId
);
},
},
},
});
return updated;
}
}
},
},
}),
},
};
+14 -6
View File
@@ -1,8 +1,8 @@
import React from 'react';
import styles from './styles.css';
import {withReaction} from 'plugin-api/beta/client/hocs';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import { withReaction } from 'plugin-api/beta/client/hocs';
import { t } from 'plugin-api/beta/client/services';
import { Icon } from 'plugin-api/beta/client/components/ui';
import cn from 'classnames';
const plugin = 'talk-plugin-like';
@@ -31,15 +31,23 @@ class LikeButton extends React.Component {
};
render() {
const {count, alreadyReacted} = this.props;
const { count, alreadyReacted } = this.props;
return (
<div className={cn(styles.container, `${plugin}-container`)}>
<button
className={cn(styles.button, {[`${styles.liked} talk-plugin-like-liked`]: alreadyReacted}, `${plugin}-button`)}
className={cn(
styles.button,
{ [`${styles.liked} talk-plugin-like-liked`]: alreadyReacted },
`${plugin}-button`
)}
onClick={this.handleClick}
>
<span className={cn(`${plugin}-label`, styles.label)}>
{t(alreadyReacted ? 'talk-plugin-like.liked' : 'talk-plugin-like.like')}
{t(
alreadyReacted
? 'talk-plugin-like.liked'
: 'talk-plugin-like.like'
)}
</span>
<Icon name="thumb_up" className={cn(`${plugin}-icon`, styles.icon)} />
<span className={`${plugin}-count`}>{count > 0 && count}</span>
+1 -1
View File
@@ -4,6 +4,6 @@ import translations from './translations.yml';
export default {
translations,
slots: {
commentReactions: [LikeButton]
commentReactions: [LikeButton],
},
};
+1 -1
View File
@@ -1,2 +1,2 @@
const {getReactionConfig} = require('../../plugin-api/beta/server');
const { getReactionConfig } = require('../../plugin-api/beta/server');
module.exports = getReactionConfig('like');
+14 -6
View File
@@ -1,8 +1,8 @@
import React from 'react';
import styles from './styles.css';
import {withReaction} from 'plugin-api/beta/client/hocs';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import { withReaction } from 'plugin-api/beta/client/hocs';
import { t } from 'plugin-api/beta/client/services';
import { Icon } from 'plugin-api/beta/client/components/ui';
import cn from 'classnames';
const plugin = 'talk-plugin-love';
@@ -31,15 +31,23 @@ class LoveButton extends React.Component {
};
render() {
const {count, alreadyReacted} = this.props;
const { count, alreadyReacted } = this.props;
return (
<div className={cn(styles.container, `${plugin}-container`)}>
<button
className={cn(styles.button, {[styles.loved]: alreadyReacted}, `${plugin}-button`)}
className={cn(
styles.button,
{ [styles.loved]: alreadyReacted },
`${plugin}-button`
)}
onClick={this.handleClick}
>
<span className={cn(`${plugin}-label`, styles.label)}>
{t(alreadyReacted ? 'talk-plugin-love.loved' : 'talk-plugin-love.love')}
{t(
alreadyReacted
? 'talk-plugin-love.loved'
: 'talk-plugin-love.love'
)}
</span>
<Icon name="favorite" className={cn(`${plugin}-icon`, styles.icon)} />
<span className={`${plugin}-count`}>{count > 0 && count}</span>
+2 -2
View File
@@ -4,6 +4,6 @@ import translations from './translations.yml';
export default {
translations,
slots: {
commentReactions: [LoveButton]
}
commentReactions: [LoveButton],
},
};
+1 -1
View File
@@ -1,2 +1,2 @@
const {getReactionConfig} = require('../../plugin-api/beta/server');
const { getReactionConfig } = require('../../plugin-api/beta/server');
module.exports = getReactionConfig('love');
@@ -1,15 +1,19 @@
import React from 'react';
import styles from './MemberSinceInfo.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import { t } from 'plugin-api/beta/client/services';
import { Icon } from 'plugin-api/beta/client/components/ui';
import cn from 'classnames';
import moment from 'moment';
export default ({memberSinceDate}) => (
export default ({ memberSinceDate }) => (
<div className={cn(styles.root, 'talk-plugin-member-since')}>
<Icon name="date_range" className={cn(styles.icon, 'talk-plugin-member-since-icon')} />
<Icon
name="date_range"
className={cn(styles.icon, 'talk-plugin-member-since-icon')}
/>
<span className={cn(styles.memberSince, 'talk-plugin-member-since-date')}>
{t('talk-plugin-member-since.member_since')}: {moment(new Date(memberSinceDate)).format('MMM DD, YYYY')}
{t('talk-plugin-member-since.member_since')}:{' '}
{moment(new Date(memberSinceDate)).format('MMM DD, YYYY')}
</span>
</div>
);
@@ -1,13 +1,13 @@
import React from 'react';
import MemberSinceInfo from '../components/MemberSinceInfo';
import {compose, gql} from 'react-apollo';
import {withFragments} from 'plugin-api/beta/client/hocs';
import { compose, gql } from 'react-apollo';
import { withFragments } from 'plugin-api/beta/client/hocs';
class MemberSinceInfoContainer extends React.Component {
render() {
return <MemberSinceInfo
memberSinceDate={this.props.comment.user.created_at}
/>;
return (
<MemberSinceInfo memberSinceDate={this.props.comment.user.created_at} />
);
}
}
@@ -18,11 +18,10 @@ const withMemberSinceInfoFragments = withFragments({
username
created_at
}
}`,
}
`,
});
const enhance = compose(
withMemberSinceInfoFragments,
);
const enhance = compose(withMemberSinceInfoFragments);
export default enhance(MemberSinceInfoContainer);
@@ -1,10 +1,10 @@
import MemberSinceInfo from './containers/MemberSinceInfo';
import translations from './translations.yml';
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
export default {
slots: {
authorMenuInfos: [MemberSinceInfo]
authorMenuInfos: [MemberSinceInfo],
},
translations,
fragments: {
@@ -15,7 +15,8 @@ export default {
created_at
}
}
}`,
}
`,
},
mutations: {
PostComment: () => ({
@@ -24,10 +25,10 @@ export default {
comment: {
user: {
created_at: new Date(),
__typename: 'User'
__typename: 'User',
},
__typename: 'Comment'
}
__typename: 'Comment',
},
},
},
}),
@@ -1,6 +1,11 @@
import {OPEN_MENU, CLOSE_MENU, OPEN_BAN_DIALOG, CLOSE_BAN_DIALOG} from './constants';
import {
OPEN_MENU,
CLOSE_MENU,
OPEN_BAN_DIALOG,
CLOSE_BAN_DIALOG,
} from './constants';
export const openMenu = (id) => ({
export const openMenu = id => ({
type: OPEN_MENU,
id,
});
@@ -9,11 +14,11 @@ export const closeMenu = () => ({
type: CLOSE_MENU,
});
export const openBanDialog = ({commentId, authorId, commentStatus}) => ({
export const openBanDialog = ({ commentId, authorId, commentStatus }) => ({
type: OPEN_BAN_DIALOG,
commentId,
authorId,
commentStatus
commentStatus,
});
export const closeBanDialog = () => ({
@@ -1,25 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './styles.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import { t } from 'plugin-api/beta/client/services';
import { Icon } from 'plugin-api/beta/client/components/ui';
import cn from 'classnames';
const isApproved = (status) => (status === 'ACCEPTED');
const isApproved = status => status === 'ACCEPTED';
const ApproveCommentAction = ({approveComment, comment: {status}}) => (
const ApproveCommentAction = ({ approveComment, comment: { status } }) =>
isApproved(status) ? (
<span className={styles.approved}>
<Icon name="check_circle" className={styles.icon} />
{t('talk-plugin-moderation-actions.approved_comment')}
</span>
) : (
<button className={cn(styles.button, 'talk-plugin-moderation-actions-approve')} onClick={approveComment}>
<button
className={cn(styles.button, 'talk-plugin-moderation-actions-approve')}
onClick={approveComment}
>
<Icon name="done" className={styles.icon} />
{t('talk-plugin-moderation-actions.approve_comment')}
</button>
)
);
);
ApproveCommentAction.propTypes = {
approveComment: PropTypes.func,
@@ -1,14 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './styles.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import { t } from 'plugin-api/beta/client/services';
import { Icon } from 'plugin-api/beta/client/components/ui';
import cn from 'classnames';
const BanUserAction = ({onBanUser}) => (
<button
const BanUserAction = ({ onBanUser }) => (
<button
className={cn(styles.button, 'talk-plugin-moderation-actions-ban')}
onClick={onBanUser} >
onClick={onBanUser}
>
<Icon name="block" className={styles.icon} />
{t('talk-plugin-moderation-actions.ban_user')}
</button>
@@ -2,12 +2,17 @@ import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './BanUserDialog.css';
import {t} from 'plugin-api/beta/client/services';
import {Dialog, Button} from 'plugin-api/beta/client/components/ui';
import { t } from 'plugin-api/beta/client/services';
import { Dialog, Button } from 'plugin-api/beta/client/components/ui';
const BanUserDialog = ({showBanDialog, closeBanDialog, banUser}) => (
<Dialog open={showBanDialog} className={cn(styles.dialog, 'talk-ban-user-dialog')}>
<span className={styles.close} onClick={closeBanDialog}>×</span>
const BanUserDialog = ({ showBanDialog, closeBanDialog, banUser }) => (
<Dialog
open={showBanDialog}
className={cn(styles.dialog, 'talk-ban-user-dialog')}
>
<span className={styles.close} onClick={closeBanDialog}>
×
</span>
<h2>{t('talk-plugin-moderation-actions.ban_user_dialog_headline')}</h2>
<h3>{t('talk-plugin-moderation-actions.ban_user_dialog_sub')}</h3>
<p className={styles.copy}>
@@ -18,14 +23,16 @@ const BanUserDialog = ({showBanDialog, closeBanDialog, banUser}) => (
cStyle="cancel"
onClick={closeBanDialog}
className={cn(styles.cancel, 'talk-ban-user-dialog-button-cancel')}
raised >
raised
>
{t('talk-plugin-moderation-actions.ban_user_dialog_cancel')}
</Button>
<Button
cStyle="black"
onClick={banUser}
className={cn(styles.confirm, 'talk-ban-user-dialog-button-confirm')}
raised >
raised
>
{t('talk-plugin-moderation-actions.ban_user_dialog_yes')}
</Button>
</div>
@@ -2,9 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './Menu.css';
import {t} from 'plugin-api/beta/client/services';
import { t } from 'plugin-api/beta/client/services';
const Menu = ({className = '', children}) => (
const Menu = ({ className = '', children }) => (
<div className={cn(styles.menu, className)}>
<h3 className={styles.headline}>
{t('talk-plugin-moderation-actions.moderation_actions')}
@@ -3,35 +3,58 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import Menu from './Menu';
import styles from './ModerationActions.css';
import {Icon} from 'plugin-api/beta/client/components/ui';
import { Icon } from 'plugin-api/beta/client/components/ui';
import ClickOutside from 'coral-framework/components/ClickOutside';
import RejectCommentAction from '../containers/RejectCommentAction';
import ApproveCommentAction from '../containers/ApproveCommentAction';
import BanUserAction from '../containers/BanUserAction';
import {Slot} from 'plugin-api/beta/client/components';
import { Slot } from 'plugin-api/beta/client/components';
export default class ModerationActions extends React.Component {
render() {
const {comment, root, asset, data, menuVisible, toogleMenu, hideMenu} = this.props;
const {
comment,
root,
asset,
data,
menuVisible,
toogleMenu,
hideMenu,
} = this.props;
return(
return (
<ClickOutside onClickOutside={hideMenu}>
<div className={cn(styles.moderationActions, 'talk-plugin-moderation-actions')}>
<span onClick={toogleMenu} className={cn(styles.arrow, 'talk-plugin-moderation-actions-arrow')}>
{menuVisible ? <Icon name="keyboard_arrow_up" className={styles.icon} /> :
<Icon name="keyboard_arrow_down" className={styles.icon} />}
<div
className={cn(
styles.moderationActions,
'talk-plugin-moderation-actions'
)}
>
<span
onClick={toogleMenu}
className={cn(styles.arrow, 'talk-plugin-moderation-actions-arrow')}
>
{menuVisible ? (
<Icon name="keyboard_arrow_up" className={styles.icon} />
) : (
<Icon name="keyboard_arrow_down" className={styles.icon} />
)}
</span>
{menuVisible && (
<Menu className="talk-plugin-modetarion-actions-menu">
<Slot
className="talk-plugin-modetarion-actions-slot"
fill="moderationActions"
queryData={{comment, asset}}
queryData={{ comment, asset }}
data={data}
/>
<ApproveCommentAction comment={comment} hideMenu={hideMenu} />
<RejectCommentAction comment={comment} hideMenu={hideMenu} />
<BanUserAction comment={comment} root={root} hideMenu={hideMenu} />
<BanUserAction
comment={comment}
root={root}
hideMenu={hideMenu}
/>
</Menu>
)}
</div>
@@ -2,11 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './styles.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import { t } from 'plugin-api/beta/client/services';
import { Icon } from 'plugin-api/beta/client/components/ui';
const RejectCommentAction = ({rejectComment}) => (
<button className={cn(styles.button, 'talk-plugin-moderation-actions-reject')} onClick={rejectComment}>
const RejectCommentAction = ({ rejectComment }) => (
<button
className={cn(styles.button, 'talk-plugin-moderation-actions-reject')}
onClick={rejectComment}
>
<Icon name="clear" className={styles.icon} />
{t('talk-plugin-moderation-actions.reject_comment')}
</button>
@@ -1,38 +1,44 @@
import React from 'react';
import {compose} from 'react-apollo';
import {bindActionCreators} from 'redux';
import {getErrorMessages} from 'plugin-api/beta/client/utils';
import {notify} from 'plugin-api/beta/client/actions/notification';
import { compose } from 'react-apollo';
import { bindActionCreators } from 'redux';
import { getErrorMessages } from 'plugin-api/beta/client/utils';
import { notify } from 'plugin-api/beta/client/actions/notification';
import ApproveCommentAction from '../components/ApproveCommentAction';
import {connect, withSetCommentStatus} from 'plugin-api/beta/client/hocs';
import { connect, withSetCommentStatus } from 'plugin-api/beta/client/hocs';
class ApproveCommentActionContainer extends React.Component {
approveComment = async () => {
const {setCommentStatus, comment, hideMenu, notify} = this.props;
const { setCommentStatus, comment, hideMenu, notify } = this.props;
try {
await setCommentStatus({
commentId: comment.id,
status: 'ACCEPTED'
status: 'ACCEPTED',
});
}
catch(err) {
} catch (err) {
notify('error', getErrorMessages(err));
}
hideMenu();
}
};
render() {
return <ApproveCommentAction comment={this.props.comment} approveComment={this.approveComment}/>;
return (
<ApproveCommentAction
comment={this.props.comment}
approveComment={this.approveComment}
/>
);
}
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
notify
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
notify,
},
dispatch
);
const enhance = compose(
connect(null, mapDispatchToProps),
@@ -1,36 +1,35 @@
import React from 'react';
import {compose} from 'react-apollo';
import {openBanDialog} from '../actions';
import {bindActionCreators} from 'redux';
import { compose } from 'react-apollo';
import { openBanDialog } from '../actions';
import { bindActionCreators } from 'redux';
import BanUserAction from '../components/BanUserAction';
import {connect} from 'plugin-api/beta/client/hocs';
import { connect } from 'plugin-api/beta/client/hocs';
class BanUserActionContainer extends React.Component {
onBanUser = () => {
this.props.openBanDialog({
commentId: this.props.comment.id,
commentStatus: this.props.comment.status,
authorId: this.props.comment.user.id
authorId: this.props.comment.user.id,
});
}
};
render() {
const {root: {me}, comment} = this.props;
return (me.id !== comment.user.id) ?
<BanUserAction
onBanUser={this.onBanUser}
comment={this.props.comment}
/> : null;
const { root: { me }, comment } = this.props;
return me.id !== comment.user.id ? (
<BanUserAction onBanUser={this.onBanUser} comment={this.props.comment} />
) : null;
}
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
openBanDialog
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
openBanDialog,
},
dispatch
);
const enhance = compose(
connect(null, mapDispatchToProps)
);
const enhance = compose(connect(null, mapDispatchToProps));
export default enhance(BanUserActionContainer);
@@ -1,16 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import {compose} from 'react-apollo';
import {bindActionCreators} from 'redux';
import {closeBanDialog, closeMenu} from '../actions';
import {notify} from 'plugin-api/beta/client/actions/notification';
import {connect, withSetCommentStatus, withBanUser} from 'plugin-api/beta/client/hocs';
import {getErrorMessages} from 'plugin-api/beta/client/utils';
import { compose } from 'react-apollo';
import { bindActionCreators } from 'redux';
import { closeBanDialog, closeMenu } from '../actions';
import { notify } from 'plugin-api/beta/client/actions/notification';
import {
connect,
withSetCommentStatus,
withBanUser,
} from 'plugin-api/beta/client/hocs';
import { getErrorMessages } from 'plugin-api/beta/client/utils';
import BanUserDialog from '../components/BanUserDialog';
class BanUserDialogContainer extends React.Component {
class BanUserDialogContainer extends React.Component {
banUser = async () => {
const {
notify,
@@ -35,19 +37,18 @@ class BanUserDialogContainer extends React.Component {
if (commentStatus !== 'REJECTED') {
await setCommentStatus({
commentId: commentId,
status: 'REJECTED'
status: 'REJECTED',
});
}
}
catch(err) {
} catch (err) {
notify('error', getErrorMessages(err));
}
}
};
render() {
return (
<BanUserDialog
banUser={this.banUser}
<BanUserDialog
banUser={this.banUser}
showBanDialog={this.props.showBanDialog}
closeBanDialog={this.props.closeBanDialog}
/>
@@ -60,23 +61,26 @@ BanUserDialogContainer.propTypes = {
closeBanDialog: PropTypes.func,
};
const mapStateToProps = ({talkPluginModerationActions: state}) => ({
const mapStateToProps = ({ talkPluginModerationActions: state }) => ({
showBanDialog: state.showBanDialog,
commentId: state.commentId,
authorId: state.authorId,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
notify,
closeBanDialog,
closeMenu,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
notify,
closeBanDialog,
closeMenu,
},
dispatch
);
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withSetCommentStatus,
withBanUser,
withBanUser
);
export default enhance(BanUserDialogContainer);
@@ -1,16 +1,14 @@
import React from 'react';
import {bindActionCreators} from 'redux';
import {gql, compose} from 'react-apollo';
import {openMenu, closeMenu} from '../actions';
import {can} from 'plugin-api/beta/client/services';
import {getShallowChanges} from 'plugin-api/beta/client/utils';
import { bindActionCreators } from 'redux';
import { gql, compose } from 'react-apollo';
import { openMenu, closeMenu } from '../actions';
import { can } from 'plugin-api/beta/client/services';
import { getShallowChanges } from 'plugin-api/beta/client/utils';
import ModerationActions from '../components/ModerationActions';
import {connect, excludeIf, withFragments} from 'plugin-api/beta/client/hocs';
import { connect, excludeIf, withFragments } from 'plugin-api/beta/client/hocs';
class ModerationActionsContainer extends React.Component {
shouldComponentUpdate(nextProps) {
// Specifically handle `showMenuForComment` if it is the only change.
const changes = getShallowChanges(this.props, nextProps);
if (changes.length === 1 && changes[0] === 'showMenuForComment') {
@@ -22,7 +20,7 @@ class ModerationActionsContainer extends React.Component {
return false;
}
}
// Prevent Slot from rerendering when no props has shallowly changed.
return changes.length !== 0;
}
@@ -33,37 +31,42 @@ class ModerationActionsContainer extends React.Component {
} else {
this.props.openMenu(this.props.comment.id);
}
}
};
hideMenu = () => {
if (this.props.showMenuForComment === this.props.comment.id) {
this.props.closeMenu();
}
}
};
render() {
return <ModerationActions
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
comment={this.props.comment}
toogleMenu={this.toogleMenu}
hideMenu={this.hideMenu}
menuVisible={this.props.showMenuForComment === this.props.comment.id}
/>;
return (
<ModerationActions
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
comment={this.props.comment}
toogleMenu={this.toogleMenu}
hideMenu={this.hideMenu}
menuVisible={this.props.showMenuForComment === this.props.comment.id}
/>
);
}
}
const mapStateToProps = ({auth, talkPluginModerationActions: state}) => ({
const mapStateToProps = ({ auth, talkPluginModerationActions: state }) => ({
user: auth.user,
showMenuForComment: state.showMenuForComment,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
openMenu,
closeMenu,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
openMenu,
closeMenu,
},
dispatch
);
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
@@ -73,13 +76,13 @@ const enhance = compose(
me {
id
}
}`
,
}
`,
asset: gql`
fragment TalkModerationActions_asset on Asset {
id
}`
,
}
`,
comment: gql`
fragment TalkModerationActions_comment on Comment {
id
@@ -93,8 +96,9 @@ const enhance = compose(
}
}
}
`}),
excludeIf((props) => !can(props.user, 'MODERATE_COMMENTS')),
`,
}),
excludeIf(props => !can(props.user, 'MODERATE_COMMENTS'))
);
export default enhance(ModerationActionsContainer);
@@ -1,38 +1,39 @@
import React from 'react';
import {compose} from 'react-apollo';
import {bindActionCreators} from 'redux';
import {getErrorMessages} from 'plugin-api/beta/client/utils';
import {notify} from 'plugin-api/beta/client/actions/notification';
import { compose } from 'react-apollo';
import { bindActionCreators } from 'redux';
import { getErrorMessages } from 'plugin-api/beta/client/utils';
import { notify } from 'plugin-api/beta/client/actions/notification';
import RejectCommentAction from '../components/RejectCommentAction';
import {connect, withSetCommentStatus} from 'plugin-api/beta/client/hocs';
import { connect, withSetCommentStatus } from 'plugin-api/beta/client/hocs';
class RejectCommentActionContainer extends React.Component {
rejectComment = async () => {
const {setCommentStatus, comment, hideMenu, notify} = this.props;
const { setCommentStatus, comment, hideMenu, notify } = this.props;
try {
await setCommentStatus({
commentId: comment.id,
status: 'REJECTED'
status: 'REJECTED',
});
}
catch(err) {
} catch (err) {
notify('error', getErrorMessages(err));
}
hideMenu();
}
};
render() {
return <RejectCommentAction rejectComment={this.rejectComment}/>;
return <RejectCommentAction rejectComment={this.rejectComment} />;
}
}
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
notify
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
notify,
},
dispatch
);
const enhance = compose(
connect(null, mapDispatchToProps),
@@ -9,5 +9,5 @@ export default {
commentInfoBar: [ModerationActions],
},
reducer,
translations
translations,
};
@@ -1,42 +1,47 @@
import {OPEN_MENU, CLOSE_MENU, OPEN_BAN_DIALOG, CLOSE_BAN_DIALOG} from './constants';
import {
OPEN_MENU,
CLOSE_MENU,
OPEN_BAN_DIALOG,
CLOSE_BAN_DIALOG,
} from './constants';
const initialState = {
showMenuForComment: null,
showBanDialog: false,
authorId: null,
commentId: null,
commentStatus: null
commentStatus: null,
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case OPEN_MENU:
return {
...state,
showMenuForComment: action.id
};
case CLOSE_MENU:
return {
...state,
showMenuForComment: null
};
case OPEN_BAN_DIALOG:
return {
...state,
showBanDialog: true,
authorId: action.authorId,
commentId: action.commentId,
commentStatus: action.commentStatus
};
case CLOSE_BAN_DIALOG:
return {
...state,
showBanDialog: false,
authorId: null,
commentId: null,
commentStatus: null
};
default :
return state;
case OPEN_MENU:
return {
...state,
showMenuForComment: action.id,
};
case CLOSE_MENU:
return {
...state,
showMenuForComment: null,
};
case OPEN_BAN_DIALOG:
return {
...state,
showBanDialog: true,
authorId: action.authorId,
commentId: action.commentId,
commentStatus: action.commentStatus,
};
case CLOSE_BAN_DIALOG:
return {
...state,
showBanDialog: false,
authorId: null,
commentId: null,
commentStatus: null,
};
default:
return state;
}
}
@@ -1,5 +1,5 @@
import {OFFTOPIC_TOGGLE_CHECKBOX} from './constants';
import { OFFTOPIC_TOGGLE_CHECKBOX } from './constants';
export const toggleCheckbox = () => ({
type: OFFTOPIC_TOGGLE_CHECKBOX
type: OFFTOPIC_TOGGLE_CHECKBOX,
});
@@ -1,10 +1,9 @@
import React from 'react';
import styles from './OffTopicCheckbox.css';
import {t} from 'plugin-api/beta/client/services';
import { t } from 'plugin-api/beta/client/services';
export default class OffTopicCheckbox extends React.Component {
label = 'OFF_TOPIC';
componentDidMount() {
@@ -18,28 +17,30 @@ export default class OffTopicCheckbox extends React.Component {
this.props.unregisterHook(this.clearTagsHook);
}
handleChange = (e) => {
const {addTag, removeTag} = this.props;
handleChange = e => {
const { addTag, removeTag } = this.props;
if (e.target.checked) {
addTag(this.label);
} else {
const idx = this.props.tags.indexOf(this.label);
removeTag(idx);
}
}
};
render() {
const checked = this.props.tags.indexOf(this.label) >= 0;
return (
<div className={styles.offTopic}>
{
!this.props.isReply ? (
<label className={styles.offTopicLabel}>
<input type="checkbox" onChange={this.handleChange} checked={checked}/>
{t('off_topic')}
</label>
) : null
}
{!this.props.isReply ? (
<label className={styles.offTopicLabel}>
<input
type="checkbox"
onChange={this.handleChange}
checked={checked}
/>
{t('off_topic')}
</label>
) : null}
</div>
);
}
@@ -1,23 +1,24 @@
import React from 'react';
import styles from './OffTopicFilter.css';
import {t} from 'plugin-api/beta/client/services';
import { t } from 'plugin-api/beta/client/services';
export default class OffTopicFilter extends React.Component {
tag = 'OFF_TOPIC';
className = 'talk-plugin-off-topic-comment';
cn = {[this.className] : {tags: [this.tag]}};
cn = { [this.className]: { tags: [this.tag] } };
handleChange = (e) => {
handleChange = e => {
if (e.target.checked) {
this.props.addCommentClassName(this.cn);
this.props.toggleCheckbox();
} else {
const idx = this.props.commentClassNames.findIndex((i) => i[this.className]);
const idx = this.props.commentClassNames.findIndex(
i => i[this.className]
);
this.props.removeCommentClassName(idx);
this.props.toggleCheckbox();
}
}
};
render() {
return (
@@ -1,17 +1,17 @@
import React from 'react';
import styles from './OffTopicTag.css';
import {t} from 'plugin-api/beta/client/services';
import {isTagged} from 'plugin-api/beta/client/utils';
import { t } from 'plugin-api/beta/client/services';
import { isTagged } from 'plugin-api/beta/client/utils';
import cn from 'classnames';
export default (props) => (
export default props => (
<span>
{
isTagged(props.comment.tags, 'OFF_TOPIC') && props.depth === 0 ? (
<span className={cn(styles.tag, 'talk-stream-comment-offtopic-tag-label')}>
{t('off_topic')}
</span>
) : null
}
{isTagged(props.comment.tags, 'OFF_TOPIC') && props.depth === 0 ? (
<span
className={cn(styles.tag, 'talk-stream-comment-offtopic-tag-label')}
>
{t('off_topic')}
</span>
) : null}
</span>
);
@@ -1,14 +1,14 @@
import {bindActionCreators} from 'redux';
import {addTag, removeTag} from 'plugin-api/alpha/client/actions';
import {commentBoxTagsSelector} from 'plugin-api/alpha/client/selectors';
import {connect} from 'plugin-api/beta/client/hocs';
import { bindActionCreators } from 'redux';
import { addTag, removeTag } from 'plugin-api/alpha/client/actions';
import { commentBoxTagsSelector } from 'plugin-api/alpha/client/selectors';
import { connect } from 'plugin-api/beta/client/hocs';
import OffTopicCheckbox from '../components/OffTopicCheckbox';
const mapStateToProps = (state) => ({
tags: commentBoxTagsSelector(state)
const mapStateToProps = state => ({
tags: commentBoxTagsSelector(state),
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({addTag, removeTag}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators({ addTag, removeTag }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(OffTopicCheckbox);
@@ -1,24 +1,24 @@
import {connect} from 'plugin-api/beta/client/hocs';
import {bindActionCreators} from 'redux';
import {toggleCheckbox} from '../actions';
import {commentClassNamesSelector} from 'plugin-api/alpha/client/selectors';
import { connect } from 'plugin-api/beta/client/hocs';
import { bindActionCreators } from 'redux';
import { toggleCheckbox } from '../actions';
import { commentClassNamesSelector } from 'plugin-api/alpha/client/selectors';
import OffTopicFilter from '../components/OffTopicFilter';
import {
addCommentClassName,
removeCommentClassName
removeCommentClassName,
} from 'plugin-api/alpha/client/actions';
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
commentClassNames: commentClassNamesSelector(state),
checked: state.talkPluginOfftopic.checked
checked: state.talkPluginOfftopic.checked,
});
const mapDispatchToProps = (dispatch) =>
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
toggleCheckbox,
addCommentClassName,
removeCommentClassName
removeCommentClassName,
},
dispatch
);
@@ -1,6 +1,6 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import OffTopicTag from '../components/OffTopicTag';
import {withFragments} from 'plugin-api/beta/client/hocs';
import { withFragments } from 'plugin-api/beta/client/hocs';
export default withFragments({
comment: gql`
@@ -11,5 +11,5 @@ export default withFragments({
}
}
}
`
`,
})(OffTopicTag);
+2 -2
View File
@@ -15,6 +15,6 @@ export default {
slots: {
commentInputDetailArea: [OffTopicCheckbox],
commentInfoBar: [OffTopicTag],
viewingOptionsFilter: [OffTopicFilter]
}
viewingOptionsFilter: [OffTopicFilter],
},
};
+11 -11
View File
@@ -1,18 +1,18 @@
import {OFFTOPIC_TOGGLE_CHECKBOX} from './constants';
import { OFFTOPIC_TOGGLE_CHECKBOX } from './constants';
const initialState = {
checked: false
checked: false,
};
export default function offTopic (state = initialState, action) {
export default function offTopic(state = initialState, action) {
switch (action.type) {
case OFFTOPIC_TOGGLE_CHECKBOX: {
return {
...state,
checked: !state.checked
};
}
default :
return state;
case OFFTOPIC_TOGGLE_CHECKBOX: {
return {
...state,
checked: !state.checked,
};
}
default:
return state;
}
}
+4 -4
View File
@@ -5,10 +5,10 @@ module.exports = {
permissions: {
public: true,
self: true,
roles: []
roles: [],
},
models: ['COMMENTS'],
created_at: new Date()
}
]
created_at: new Date(),
},
],
};
@@ -1,39 +1,38 @@
import React from 'react';
import cn from 'classnames';
import styles from './styles.css';
import {t} from 'plugin-api/beta/client/services';
import {ClickOutside} from 'plugin-api/beta/client/components';
import {Icon, Button} from 'plugin-api/beta/client/components/ui';
import { t } from 'plugin-api/beta/client/services';
import { ClickOutside } from 'plugin-api/beta/client/components';
import { Icon, Button } from 'plugin-api/beta/client/components/ui';
const name = 'talk-plugin-permalink';
export default class PermalinkButton extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
popoverOpen: false,
copySuccessful: null,
copyFailure: null
copyFailure: null,
};
}
toggle = () => {
this.popover.style.top = `${this.linkButton.offsetTop - 80}px`;
this.setState({
popoverOpen: !this.state.popoverOpen
popoverOpen: !this.state.popoverOpen,
});
}
};
handleClickOutside = () => {
if (this.state.popoverOpen) {
this.setState({
popoverOpen: false
popoverOpen: false,
});
}
}
};
copyPermalink = () => {
this.permalinkInput.select();
@@ -41,52 +40,56 @@ export default class PermalinkButton extends React.Component {
document.execCommand('copy');
this.setState({
copySuccessful: true,
copyFailure: null
copyFailure: null,
});
} catch (err) {
this.setState({
copyFailure: true,
copySuccessful: null
copySuccessful: null,
});
}
this.timeout = window.setTimeout(() => {
this.setState({
copyFailure: null,
copySuccessful: null
copySuccessful: null,
});
}, 3000);
}
};
componentWillUnmount() {
window.clearTimeout(this.timeout);
}
render () {
const {copySuccessful, copyFailure, popoverOpen} = this.state;
const {asset, comment} = this.props;
render() {
const { copySuccessful, copyFailure, popoverOpen } = this.state;
const { asset, comment } = this.props;
return (
<ClickOutside onClickOutside={this.handleClickOutside}>
<div className={cn(`${name}-container`, styles.container)}>
<button
ref={(ref) => this.linkButton = ref}
ref={ref => (this.linkButton = ref)}
onClick={this.toggle}
className={cn(`${name}-button`, styles.button)}>
<span className='talk-plugin-permalink-button-label'>
className={cn(`${name}-button`, styles.button)}
>
<span className="talk-plugin-permalink-button-label">
{t('permalink')}
</span>
<Icon name="link" className={styles.icon}/>
<Icon name="link" className={styles.icon} />
</button>
<div
ref={(ref) => this.popover = ref}
className={cn([`${name}-popover`, styles.popover, {[styles.active]: popoverOpen}])}>
ref={ref => (this.popover = ref)}
className={cn([
`${name}-popover`,
styles.popover,
{ [styles.active]: popoverOpen },
])}
>
<input
className={cn(styles.input, `${name}-copy-field`)}
type='text'
ref={(input) => this.permalinkInput = input}
type="text"
ref={input => (this.permalinkInput = input)}
defaultValue={`${asset.url}?commentId=${comment.id}`}
readOnly
/>
@@ -95,15 +98,17 @@ export default class PermalinkButton extends React.Component {
onClick={this.copyPermalink}
className={cn([
styles.copyButton,
`${name}-copy-button`, {
[styles.success]:copySuccessful,
[styles.failure]: copyFailure
}])}>
`${name}-copy-button`,
{
[styles.success]: copySuccessful,
[styles.failure]: copyFailure,
},
])}
>
{!copyFailure && !copySuccessful && 'Copy'}
{copySuccessful && 'Copied'}
{copyFailure && 'Not supported'}
</Button>
</div>
</div>
</ClickOutside>
@@ -1,16 +1,16 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import PermalinkButton from '../components/PermalinkButton';
import {withFragments} from 'plugin-api/beta/client/hocs';
import { withFragments } from 'plugin-api/beta/client/hocs';
export default withFragments({
asset: gql`
fragment TalkPermalink_Button_asset on Asset {
url
}
`,
`,
comment: gql`
fragment TalkPermalink_Button_comment on Comment {
id
}
`
`,
})(PermalinkButton);
@@ -2,6 +2,6 @@ import PermalinkButton from './containers/PermalinkButton';
export default {
slots: {
commentActions: [PermalinkButton]
}
commentActions: [PermalinkButton],
},
};
@@ -1,11 +1,13 @@
import {setSort} from 'plugin-api/beta/client/actions/stream';
import {sortOrderSelector, sortBySelector} from 'plugin-api/beta/client/selectors/stream';
import { setSort } from 'plugin-api/beta/client/actions/stream';
import {
sortOrderSelector,
sortBySelector,
} from 'plugin-api/beta/client/selectors/stream';
const STORAGE_PATH = 'talkPluginRememberSort';
export default {
init: async ({store, pymStorage, introspection}) => {
init: async ({ store, pymStorage, introspection }) => {
// TODO: workaround as this plugin is included in any target and
// embeds (e.g. admin), but should only be included inside the stream.
@@ -32,9 +34,9 @@ export default {
// Save sorting choice to storage if it has changed.
if (!sort || sort.sortOrder !== sortOrder || sort.sortBy !== sortBy) {
sort = {sortOrder, sortBy};
sort = { sortOrder, sortBy };
pymStorage.setItem(STORAGE_PATH, JSON.stringify(sort));
}
});
}
},
};

Some files were not shown because too many files have changed in this diff Show More