mirror of
https://github.com/wassname/talk.git
synced 2026-07-17 11:33:39 +08:00
Merge branch 'master' into admin-login-upgrade
This commit is contained in:
@@ -20,13 +20,17 @@ const validation = (formData, dispatch, next) => {
|
||||
return dispatch(hasError());
|
||||
}
|
||||
|
||||
const validKeys = Object.keys(formData)
|
||||
.filter(name => name !== 'domains');
|
||||
|
||||
// Required Validation
|
||||
const empty = Object.keys(formData).filter(name => {
|
||||
const empty = validKeys
|
||||
.filter(name => {
|
||||
const cond = !formData[name].length;
|
||||
|
||||
if (cond) {
|
||||
|
||||
// Adding Error
|
||||
// Adding Error
|
||||
dispatch(addError(name, 'This field is required.'));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
@@ -40,18 +44,19 @@ const validation = (formData, dispatch, next) => {
|
||||
}
|
||||
|
||||
// RegExp Validation
|
||||
const validation = Object.keys(formData).filter(name => {
|
||||
const cond = !validate[name](formData[name]);
|
||||
if (cond) {
|
||||
const validation = validKeys
|
||||
.filter(name => {
|
||||
const cond = !validate[name](formData[name]);
|
||||
if (cond) {
|
||||
|
||||
// Adding Error
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
|
||||
return cond;
|
||||
});
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (validation.length) {
|
||||
return dispatch(hasError());
|
||||
@@ -71,22 +76,27 @@ export const submitSettings = () => (dispatch, getState) => {
|
||||
export const submitUser = () => (dispatch, getState) => {
|
||||
const userFormData = getState().install.toJS().data.user;
|
||||
validation(userFormData, dispatch, function() {
|
||||
const data = getState().install.toJS().data;
|
||||
dispatch(installRequest());
|
||||
coralApi('/setup', {method: 'POST', body: data})
|
||||
.then(result => {
|
||||
dispatch(installSuccess(result));
|
||||
dispatch(nextStep());
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(installFailure(`${error.translation_key}`));
|
||||
});
|
||||
dispatch(nextStep());
|
||||
});
|
||||
};
|
||||
|
||||
export const finishInstall = () => (dispatch, getState) => {
|
||||
const data = getState().install.toJS().data;
|
||||
dispatch(installRequest());
|
||||
return coralApi('/setup', {method: 'POST', body: data})
|
||||
.then(() => {
|
||||
dispatch(installSuccess());
|
||||
dispatch(nextStep());
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(installFailure(`${error.translation_key}`));
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
|
||||
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
|
||||
export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
|
||||
|
||||
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
|
||||
const checkInstallSuccess = installed => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
|
||||
export const INSTALL_FAILURE = 'INSTALL_FAILURE';
|
||||
export const UPDATE_FORMDATA_USER = 'UPDATE_FORMDATA_USER';
|
||||
export const UPDATE_FORMDATA_SETTINGS = 'UPDATE_FORMDATA_SETTINGS';
|
||||
export const UPDATE_PERMITTED_DOMAINS_SETTINGS = 'UPDATE_PERMITTED_DOMAINS_SETTINGS';
|
||||
|
||||
export const CHECK_INSTALL_REQUEST = 'CHECK_INSTALL_REQUEST';
|
||||
export const CHECK_INSTALL_SUCCESS = 'CHECK_INSTALL_SUCCESS';
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import React from 'react';
|
||||
import {Card} from 'coral-ui';
|
||||
import styles from './Configure.css';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
import styles from './Configure.css';
|
||||
import {Card} from 'coral-ui';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const Domainlist = ({domains, onChangeDomainlist}) => (
|
||||
<div>
|
||||
<h3>{lang.t('configure.domain-list-title')}</h3>
|
||||
<Card id={styles.domainlist}>
|
||||
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
|
||||
<TagsInput
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => onChangeDomainlist('whitelist', tags)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
const Domainlist = ({domains, onChangeDomainlist}) => {
|
||||
return (
|
||||
<div>
|
||||
<h3>{lang.t('configure.domain-list-title')}</h3>
|
||||
<Card id={styles.domainlist}>
|
||||
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
|
||||
<TagsInput
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => onChangeDomainlist('whitelist', tags)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Domainlist;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -5,19 +5,22 @@ import {Wizard, WizardNav} from 'coral-ui';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
|
||||
import {
|
||||
nextStep,
|
||||
previousStep,
|
||||
goToStep,
|
||||
nextStep,
|
||||
submitUser,
|
||||
checkInstall,
|
||||
previousStep,
|
||||
finishInstall,
|
||||
submitSettings,
|
||||
updateUserFormData,
|
||||
updateSettingsFormData,
|
||||
submitSettings,
|
||||
submitUser,
|
||||
checkInstall
|
||||
updatePermittedDomains
|
||||
} from '../../actions/install';
|
||||
|
||||
import InitialStep from './components/Steps/InitialStep';
|
||||
import AddOrganizationName from './components/Steps/AddOrganizationName';
|
||||
import CreateYourAccount from './components/Steps/CreateYourAccount';
|
||||
import PermittedDomainsStep from './components/Steps/PermittedDomainsStep';
|
||||
import FinalStep from './components/Steps/FinalStep';
|
||||
|
||||
class InstallContainer extends Component {
|
||||
@@ -43,6 +46,7 @@ class InstallContainer extends Component {
|
||||
<InitialStep/>
|
||||
<AddOrganizationName/>
|
||||
<CreateYourAccount/>
|
||||
<PermittedDomainsStep/>
|
||||
<FinalStep/>
|
||||
</Wizard>
|
||||
</div>
|
||||
@@ -65,10 +69,14 @@ const mapStateToProps = state => ({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
checkInstall: next => dispatch(checkInstall(next)),
|
||||
nextStep: () => dispatch(nextStep()),
|
||||
previousStep: () => dispatch(previousStep()),
|
||||
goToStep: step => dispatch(goToStep(step)),
|
||||
previousStep: () => dispatch(previousStep()),
|
||||
finishInstall: () => dispatch(finishInstall()),
|
||||
checkInstall: next => dispatch(checkInstall(next)),
|
||||
handleDomainsChange: value => {
|
||||
dispatch(updatePermittedDomains(value));
|
||||
},
|
||||
handleSettingsChange: e => {
|
||||
const {name, value} = e.currentTarget;
|
||||
dispatch(updateSettingsFormData(name, value));
|
||||
|
||||
@@ -2,25 +2,26 @@ import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {TextField, Button} from 'coral-ui';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const AddOrganizationName = props => {
|
||||
const {handleSettingsChange, handleSettingsSubmit, install} = props;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
<p>
|
||||
Please tell us the name of your organization. This will appear in emails when
|
||||
inviting new team members
|
||||
</p>
|
||||
<p>{lang.t('ADD_ORGANIZATION.DESCRIPTION')}</p>
|
||||
<div className={styles.form}>
|
||||
<form onSubmit={handleSettingsSubmit}>
|
||||
<TextField
|
||||
className={styles.TextField}
|
||||
id="organizationName"
|
||||
type="text"
|
||||
label='Organization name'
|
||||
label={lang.t('ADD_ORGANIZATION.LABEL')}
|
||||
onChange={handleSettingsChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.organizationName} />
|
||||
<Button type="submit" cStyle='black' full>Save</Button>
|
||||
<Button type="submit" cStyle='black' full>{lang.t('ADD_ORGANIZATION.SAVE')}</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,10 @@ import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {TextField, Button, Spinner} from 'coral-ui';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const InitialStep = props => {
|
||||
const {handleUserChange, handleUserSubmit, install} = props;
|
||||
return (
|
||||
@@ -12,7 +16,7 @@ const InitialStep = props => {
|
||||
className={styles.textField}
|
||||
id="email"
|
||||
type="email"
|
||||
label='Email address'
|
||||
label={lang.t('CREATE.EMAIL')}
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.email}
|
||||
@@ -23,7 +27,7 @@ const InitialStep = props => {
|
||||
className={styles.textField}
|
||||
id="username"
|
||||
type="text"
|
||||
label='Username'
|
||||
label={lang.t('CREATE.USERNAME')}
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.username}
|
||||
@@ -33,7 +37,7 @@ const InitialStep = props => {
|
||||
className={styles.textField}
|
||||
id="password"
|
||||
type="password"
|
||||
label='Password'
|
||||
label={lang.t('CREATE.PASSWORD')}
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.password}
|
||||
@@ -43,7 +47,7 @@ const InitialStep = props => {
|
||||
className={styles.textField}
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
label='Confirm Password'
|
||||
label={lang.t('CREATE.CONFIRM_PASSWORD')}
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.confirmPassword}
|
||||
@@ -51,7 +55,7 @@ const InitialStep = props => {
|
||||
|
||||
{
|
||||
!props.install.isLoading ?
|
||||
<Button cStyle='black' type="submit" full>Save</Button>
|
||||
<Button cStyle='black' type="submit" full>{lang.t('CREATE.SAVE')}</Button>
|
||||
:
|
||||
<Spinner />
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@ import styles from './style.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const InitialStep = () => {
|
||||
return (
|
||||
<div className={`${styles.step} ${styles.finalStep}`}>
|
||||
<p>
|
||||
Thanks for installing Talk! We sent an email to verify your email
|
||||
address. While you finish setting the account, you can start engaging
|
||||
with your readers now.
|
||||
</p>
|
||||
<Button raised><Link to='/admin'>Launch Talk</Link></Button>
|
||||
<Button cStyle='black' raised><a href="http://coralproject.net">Close this Installer</a></Button>
|
||||
<p>{lang.t('FINAL.DESCRIPTION')}</p>
|
||||
<Button raised><Link to='/admin'>{lang.t('FINAL.LAUNCH')}</Link></Button>
|
||||
<Button cStyle='black' raised><a href="http://coralproject.net">{lang.t('FINAL.CLOSE')}</a></Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,16 +2,16 @@ import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {Button} from 'coral-ui';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const InitialStep = props => {
|
||||
const {nextStep} = props;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
<p>
|
||||
The remainder of the Talk installation will take about ten minutes.
|
||||
Once you complete the following two steps, you will have a free
|
||||
installation and provision of Mongo and Redis.
|
||||
</p>
|
||||
<Button cStyle='green' onClick={nextStep} raised>Get Started</Button>
|
||||
<p>{lang.t('INITIAL.DESCRIPTION')}</p>
|
||||
<Button cStyle='green' onClick={nextStep} raised>{lang.t('INITIAL.SUBMIT')}</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {Button, Card} from 'coral-ui';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const PermittedDomainsStep = props => {
|
||||
const {finishInstall, install, handleDomainsChange} = props;
|
||||
const domains = install.data.settings.domains.whitelist;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
<h3>{lang.t('PERMITTED_DOMAINS.TITLE')}</h3>
|
||||
<Card className={styles.card}>
|
||||
<p>{lang.t('PERMITTED_DOMAINS.DESCRIPTION')}</p>
|
||||
<TagsInput
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => handleDomainsChange(tags)}
|
||||
/>
|
||||
</Card>
|
||||
<Button cStyle='green' onClick={finishInstall} raised>{lang.t('PERMITTED_DOMAINS.SUBMIT')}</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermittedDomainsStep;
|
||||
@@ -59,6 +59,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
max-width: 500px;
|
||||
margin: 20px auto;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.finalStep {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"en": {
|
||||
"INITIAL" : {
|
||||
"DESCRIPTION": "Let's set up your Talk community in just a few short steps.",
|
||||
"SUBMIT": "Get Started"
|
||||
},
|
||||
"ADD_ORGANIZATION": {
|
||||
"DESCRIPTION": "Please tell us the name of your organization. This will appear in emails when inviting new team members.",
|
||||
"LABEL": "Organization Name",
|
||||
"SAVE": "Save"
|
||||
},
|
||||
"CREATE": {
|
||||
"EMAIL": "Email address",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"CONFIRM_PASSWORD": "Confirm Password",
|
||||
"SAVE": "Save"
|
||||
},
|
||||
"PERMITTED_DOMAINS": {
|
||||
"TITLE": "Permitted domains",
|
||||
"DESCRIPTION": "Enter the domains you would like to permit for Talk, e.g. your local, staging and production environments (ex. localhost:3000, staging.domain.com, domain.com).",
|
||||
"SUBMIT": "Finish install"
|
||||
},
|
||||
"FINAL": {
|
||||
"DESCRIPTION": "Thanks for installing Talk! We sent an email to verify your email address. While you finish setting up the account, you can start engaging with your readers now.",
|
||||
"LAUNCH": "Launch Talk",
|
||||
"CLOSE": "Close this Installer"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"INITIAL" : {
|
||||
"DESCRIPTION": "Configuremos tu comunidad de Talk en sólo algunos pasos.",
|
||||
"SUBMIT": "Empezá!"
|
||||
},
|
||||
"ADD_ORGANIZATION": {
|
||||
"DESCRIPTION": "Por favor, dinos el nombre de tu organización. Este aparecerá en los emails cuando invites nuevos miembros.",
|
||||
"LABEL": "Nombre de la Organización",
|
||||
"SAVE": "Guardar"
|
||||
},
|
||||
"CREATE": {
|
||||
"EMAIL": "Dirección de E-Mail",
|
||||
"USERNAME": "Usuario",
|
||||
"PASSWORD": "Contraseña",
|
||||
"CONFIRM_PASSWORD": "Confirmar contraseña",
|
||||
"SAVE": "Guardar"
|
||||
},
|
||||
"PERMITTED_DOMAINS": {
|
||||
"TITLE": "Dominios Permitidos",
|
||||
"DESCRIPTION": "Agrega dominios permitidos a Talk, e.g. tu localhost, staging y ambientes de production (ex. localhost:3000, staging.domain.com, domain.com).",
|
||||
"SUBMIT": "Finalizar instalación"
|
||||
},
|
||||
"FINAL": {
|
||||
"DESCRIPTION": "Gracias por instalar Talk! Te enviamos un email para verificar tu identidad. Mientras se termina de configurar la cuenta, ya puedes empezar a interactuar con tus lectores",
|
||||
"LAUNCH": "Lanzar Talk",
|
||||
"CLOSE": "Cerrar este instalador"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Map} from 'immutable';
|
||||
import {Map, List} from 'immutable';
|
||||
|
||||
import * as actions from '../constants/install';
|
||||
|
||||
@@ -6,7 +6,10 @@ const initialState = Map({
|
||||
isLoading: false,
|
||||
data: Map({
|
||||
settings: Map({
|
||||
organizationName: ''
|
||||
organizationName: '',
|
||||
domains: Map({
|
||||
whitelist: List()
|
||||
})
|
||||
}),
|
||||
user: Map({
|
||||
username: '',
|
||||
@@ -33,6 +36,10 @@ const initialState = Map({
|
||||
{
|
||||
text: '2. Create your account',
|
||||
step: 2
|
||||
},
|
||||
{
|
||||
text: '3. Domain Whitelist',
|
||||
step: 3
|
||||
}],
|
||||
installRequest: null,
|
||||
installRequestError: null,
|
||||
@@ -50,6 +57,9 @@ export default function install (state = initialState, action) {
|
||||
case actions.GO_TO_STEP:
|
||||
return state
|
||||
.set('step', action.step);
|
||||
case actions.UPDATE_PERMITTED_DOMAINS_SETTINGS:
|
||||
return state
|
||||
.setIn(['data', 'settings', 'domains', 'whitelist'], action.value);
|
||||
case actions.UPDATE_FORMDATA_SETTINGS:
|
||||
return state
|
||||
.setIn(['data', 'settings', action.name], action.value);
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"comment-count-text-post": " characters.",
|
||||
"comment-count-error": "Please enter a valid number.",
|
||||
"domain-list-title": "Domain Whitelist",
|
||||
"domain-list-text": "Some instructions on how to type the urls."
|
||||
"domain-list-text": "Enter the domains you would like to permit for Talk, e.g. your local, staging and production environments (ex. localhost:3000, staging.domain.com, domain.com)."
|
||||
},
|
||||
"bandialog": {
|
||||
"ban_user": "Ban User?",
|
||||
@@ -213,7 +213,7 @@
|
||||
"comment-count-text-post": " caracteres",
|
||||
"comment-count-error": "Por favor escribe un número válido.",
|
||||
"domain-list-title": "Lista de Dominios Permitidos",
|
||||
"domain-list-text": "Instrucciones de como ingresar las URLs."
|
||||
"domain-list-text": "Agrega dominios permitidos a Talk, e.g. tu localhost, staging y ambientes de production (ex. localhost:3000, staging.domain.com, domain.com)."
|
||||
},
|
||||
"embedlink": {
|
||||
"copy": "Copiar"
|
||||
|
||||
@@ -43,6 +43,7 @@ class Comment extends React.Component {
|
||||
postLike: PropTypes.func.isRequired,
|
||||
deleteAction: PropTypes.func.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
highlighted: PropTypes.string,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
@@ -87,6 +88,7 @@ class Comment extends React.Component {
|
||||
addNotification,
|
||||
showSignInDialog,
|
||||
postLike,
|
||||
highlighted,
|
||||
postFlag,
|
||||
postDontAgree,
|
||||
loadMore,
|
||||
@@ -98,10 +100,12 @@ class Comment extends React.Component {
|
||||
const like = getActionSummary('LikeActionSummary', comment);
|
||||
const flag = getActionSummary('FlagActionSummary', comment);
|
||||
const dontagree = getActionSummary('DontAgreeActionSummary', comment);
|
||||
let commentClass = parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`;
|
||||
commentClass += highlighted === comment.id ? ' highlighted-comment' : '';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={parentId ? `reply ${styles.Reply}` : `comment ${styles.Comment}`}
|
||||
className={commentClass}
|
||||
id={`c_${comment.id}`}
|
||||
style={{marginLeft: depth * 30}}>
|
||||
<hr aria-hidden={true} />
|
||||
@@ -163,6 +167,7 @@ class Comment extends React.Component {
|
||||
postItem={postItem}
|
||||
depth={depth + 1}
|
||||
asset={asset}
|
||||
highlighted={highlighted}
|
||||
currentUser={currentUser}
|
||||
postLike={postLike}
|
||||
postFlag={postFlag}
|
||||
|
||||
@@ -31,12 +31,13 @@ import ChangeUsernameContainer from '../../coral-sign-in/containers/ChangeUserna
|
||||
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
|
||||
import RestrictedContent from 'coral-framework/components/RestrictedContent';
|
||||
import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer';
|
||||
import Comment from './Comment';
|
||||
import LoadMore from './LoadMore';
|
||||
import NewCount from './NewCount';
|
||||
|
||||
class Embed extends Component {
|
||||
|
||||
state = {activeTab: 0, showSignInDialog: false};
|
||||
state = {activeTab: 0, showSignInDialog: false, activeReplyBox: ''};
|
||||
|
||||
changeTab = (tab) => {
|
||||
|
||||
@@ -59,23 +60,6 @@ class Embed extends Component {
|
||||
|
||||
componentDidMount () {
|
||||
pym.sendMessage('childReady');
|
||||
|
||||
pym.onMessage('DOMContentLoaded', hash => {
|
||||
const commentId = hash.replace('#', 'c_');
|
||||
let count = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (document.getElementById(commentId)) {
|
||||
window.clearInterval(interval);
|
||||
pym.scrollParentToChildEl(commentId);
|
||||
}
|
||||
|
||||
if (++count > 100) { // ~10 seconds
|
||||
// give up waiting for the comments to load.
|
||||
// it would be weird for the page to jump after that long.
|
||||
window.clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
@@ -85,11 +69,29 @@ class Embed extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if(!isEqual(prevProps.data.comment, this.props.data.comment)) {
|
||||
|
||||
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
|
||||
setTimeout(()=>pym.scrollParentToChildEl(`c_${this.props.data.comment.id}`), 0);
|
||||
}
|
||||
}
|
||||
|
||||
setActiveReplyBox (reactKey) {
|
||||
if (!this.props.currentUser) {
|
||||
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
} else {
|
||||
this.setState({activeReplyBox: reactKey});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {activeTab} = this.state;
|
||||
const {closedAt, countCache = {}} = this.props.asset;
|
||||
const {loading, asset, refetch} = this.props.data;
|
||||
const {loading, asset, refetch, comment} = this.props.data;
|
||||
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
|
||||
const highlightedComment = comment && comment.parent ? comment.parent : comment;
|
||||
|
||||
const openStream = closedAt === null;
|
||||
|
||||
@@ -159,6 +161,28 @@ class Embed extends Component {
|
||||
}
|
||||
{!loggedIn && <SignInContainer requireEmailConfirmation={asset.settings.requireEmailConfirmation} offset={signInOffset}/>}
|
||||
{loggedIn && user && <ChangeUsernameContainer loggedIn={loggedIn} offset={signInOffset} user={user} />}
|
||||
{
|
||||
highlightedComment &&
|
||||
<Comment
|
||||
refetch={refetch}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
activeReplyBox={this.state.activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
depth={0}
|
||||
postItem={this.props.postItem}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
highlighted={comment.id}
|
||||
postLike={this.props.postLike}
|
||||
postFlag={this.props.postFlag}
|
||||
postDontAgree={this.props.postDontAgree}
|
||||
loadMore={this.props.loadMore}
|
||||
deleteAction={this.props.deleteAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
key={highlightedComment.id}
|
||||
reactKey={highlightedComment.id}
|
||||
comment={highlightedComment} />
|
||||
}
|
||||
<NewCount
|
||||
commentCount={asset.commentCount}
|
||||
countCache={countCache[asset.id]}
|
||||
@@ -171,6 +195,8 @@ class Embed extends Component {
|
||||
refetch={refetch}
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
activeReplyBox={this.state.activeReplyBox}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postLike={this.props.postLike}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {NEW_COMMENT_COUNT_POLL_INTERVAL} from 'coral-framework/constants/comment
|
||||
class Stream extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
refetch: PropTypes.func.isRequired,
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
postItem: PropTypes.func.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
@@ -19,7 +18,6 @@ class Stream extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {activeReplyBox: '', countPoll: null};
|
||||
this.setActiveReplyBox = this.setActiveReplyBox.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -42,15 +40,6 @@ class Stream extends React.Component {
|
||||
clearInterval(this.state.countPoll);
|
||||
}
|
||||
|
||||
setActiveReplyBox (reactKey) {
|
||||
if (!this.props.currentUser) {
|
||||
const offset = document.getElementById(`c_${reactKey}`).getBoundingClientRect().top - 75;
|
||||
this.props.showSignInDialog(offset);
|
||||
} else {
|
||||
this.setState({activeReplyBox: reactKey});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
comments,
|
||||
@@ -63,8 +52,7 @@ class Stream extends React.Component {
|
||||
postDontAgree,
|
||||
loadMore,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
refetch
|
||||
showSignInDialog
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -72,9 +60,8 @@ class Stream extends React.Component {
|
||||
{
|
||||
comments.map(comment =>
|
||||
<Comment
|
||||
refetch={refetch}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
activeReplyBox={this.state.activeReplyBox}
|
||||
setActiveReplyBox={this.props.setActiveReplyBox}
|
||||
activeReplyBox={this.props.activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
depth={0}
|
||||
postItem={postItem}
|
||||
|
||||
@@ -180,6 +180,11 @@ hr {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.highlighted-comment {
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid rgb(35,118,216);
|
||||
}
|
||||
|
||||
/* Tag Labels */
|
||||
|
||||
.coral-plugin-tag-label {
|
||||
|
||||
@@ -58,10 +58,11 @@
|
||||
|
||||
// ensure el has an id, as pym can't directly accept the HTMLElement
|
||||
if ( ! el.id) {el.id = '_' + String(Math.random());}
|
||||
var asset = opts.asset || window.location;
|
||||
var asset = opts.asset || window.location.href.split('#')[0];
|
||||
var comment = window.location.hash.slice(1);
|
||||
var pymParent = new pym.Parent(
|
||||
el.id,
|
||||
buildStreamIframeUrl(opts.talk, asset),
|
||||
buildStreamIframeUrl(opts.talk, asset, comment),
|
||||
{
|
||||
title: opts.title,
|
||||
asset_url: asset,
|
||||
@@ -76,14 +77,18 @@
|
||||
return Coral;
|
||||
|
||||
// build the URL to load in the pym iframe
|
||||
function buildStreamIframeUrl(talkBaseUrl, asset) {
|
||||
var iframeUrl = [
|
||||
function buildStreamIframeUrl(talkBaseUrl, asset, comment) {
|
||||
var iframeArray = [
|
||||
talkBaseUrl,
|
||||
(talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/'
|
||||
'embed/stream?asset_url=',
|
||||
encodeURIComponent(asset)
|
||||
].join('');
|
||||
return iframeUrl;
|
||||
];
|
||||
|
||||
if (comment) {
|
||||
iframeArray.push(`&comment_id=${comment}`);
|
||||
}
|
||||
return iframeArray.join('');
|
||||
}
|
||||
|
||||
// Set up postMessage listeners/handlers on the pymParent
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
query commentQuery($id: ID!) {
|
||||
comment(id: $id) {
|
||||
...commentView
|
||||
parent {
|
||||
...commentView
|
||||
replies {
|
||||
...commentView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,8 @@ export const loadMore = (data) => ({limit, cursor, parent_id, asset_id, sort}, n
|
||||
export const queryStream = graphql(STREAM_QUERY, {
|
||||
options: () => ({
|
||||
variables: {
|
||||
asset_url: getQueryVariable('asset_url')
|
||||
asset_url: getQueryVariable('asset_url'),
|
||||
comment_id: getQueryVariable('comment_id')
|
||||
}
|
||||
}),
|
||||
props: ({data}) => ({
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
query AssetQuery($asset_url: String!) {
|
||||
query AssetQuery($asset_url: String!, $comment_id: ID!) {
|
||||
comment(id: $comment_id) {
|
||||
...commentView
|
||||
parent {
|
||||
...commentView
|
||||
replies {
|
||||
...commentView
|
||||
}
|
||||
}
|
||||
}
|
||||
asset(url: $asset_url) {
|
||||
id
|
||||
title
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
border-radius: 1px;
|
||||
min-height: 24px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
+22
-10
@@ -39,7 +39,7 @@ const getCountsByAssetID = (context, asset_ids) => {
|
||||
/**
|
||||
* Returns the comment count for all comments that are public based on their
|
||||
* parent ids.
|
||||
* @param {Object} context graph context
|
||||
*
|
||||
* @param {Array<String>} parent_ids the ids of parents for which there are
|
||||
* comments that we want to get
|
||||
*/
|
||||
@@ -271,18 +271,30 @@ const genRecentComments = (_, ids) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the comment's by their id.
|
||||
* genComments returns the comments by the id's. Only admins can see non-public comments.
|
||||
* @param {Object} context graph context
|
||||
* @param {Array<String>} ids the comment id's to fetch
|
||||
* @return {Promise} resolves to the comments
|
||||
*/
|
||||
const genCommentsByID = (context, ids) => {
|
||||
return CommentModel.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
})
|
||||
.then(util.singleJoinBy(ids, 'id'));
|
||||
const genComments = ({user}, ids) => {
|
||||
let comments;
|
||||
if (user && user.hasRoles('ADMIN')) {
|
||||
comments = CommentModel.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
});
|
||||
} else {
|
||||
comments = CommentModel.find({
|
||||
id: {
|
||||
$in: ids
|
||||
},
|
||||
status: {
|
||||
$in: ['NONE', 'ACCEPTED']
|
||||
}
|
||||
});
|
||||
}
|
||||
return comments.then(util.singleJoinBy(ids, 'id'));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -292,7 +304,7 @@ const genCommentsByID = (context, ids) => {
|
||||
*/
|
||||
module.exports = (context) => ({
|
||||
Comments: {
|
||||
get: new DataLoader((ids) => genCommentsByID(context, ids)),
|
||||
get: new DataLoader((ids) => genComments(context, ids)),
|
||||
getByQuery: (query) => getCommentsByQuery(context, query),
|
||||
getCountByQuery: (query) => getCommentCountByQuery(context, query),
|
||||
countByAssetID: new util.SharedCacheDataLoader('Comments.countByAssetID', 3600, (ids) => getCountsByAssetID(context, ids)),
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
const Comment = {
|
||||
parent({parent_id}, _, {loaders: {Comments}}) {
|
||||
if (parent_id == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Comments.get.load(parent_id);
|
||||
},
|
||||
user({author_id}, _, {loaders: {Users}}) {
|
||||
return Users.getByID.load(author_id);
|
||||
},
|
||||
|
||||
@@ -38,7 +38,9 @@ const RootQuery = {
|
||||
|
||||
return Comments.getByQuery(query);
|
||||
},
|
||||
|
||||
comment(_, {id}, {loaders: {Comments}}) {
|
||||
return Comments.get.load(id);
|
||||
},
|
||||
commentCount(_, {query: {action_type, statuses, asset_id, parent_id}}, {user, loaders: {Actions, Comments}}) {
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
return null;
|
||||
|
||||
@@ -149,6 +149,9 @@ input CommentCountQuery {
|
||||
# Comment is the base representation of user interaction in Talk.
|
||||
type Comment {
|
||||
|
||||
# The parent of the comment (if there is one).
|
||||
parent: Comment
|
||||
|
||||
# The ID of the comment.
|
||||
id: ID!
|
||||
|
||||
@@ -477,6 +480,9 @@ type RootQuery {
|
||||
# Site wide settings and defaults.
|
||||
settings: Settings
|
||||
|
||||
# Finds a specific comment based on it's id.
|
||||
comment(id: ID!): Comment
|
||||
|
||||
# All assets. Requires the `ADMIN` role.
|
||||
assets: [Asset]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user