diff --git a/client/coral-admin/src/actions/install.js b/client/coral-admin/src/actions/install.js index 9fc308d2f..ceecf16c9 100644 --- a/client/coral-admin/src/actions/install.js +++ b/client/coral-admin/src/actions/install.js @@ -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}); diff --git a/client/coral-admin/src/constants/install.js b/client/coral-admin/src/constants/install.js index b3fd7e0d0..743d68593 100644 --- a/client/coral-admin/src/constants/install.js +++ b/client/coral-admin/src/constants/install.js @@ -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'; diff --git a/client/coral-admin/src/containers/Configure/Domainlist.js b/client/coral-admin/src/containers/Configure/Domainlist.js index f845dfeaa..6918c0471 100644 --- a/client/coral-admin/src/containers/Configure/Domainlist.js +++ b/client/coral-admin/src/containers/Configure/Domainlist.js @@ -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}) => ( -
-

{lang.t('configure.domain-list-title')}

- -

{lang.t('configure.domain-list-text')}

- data.split(',').map(d => d.trim())} - onChange={tags => onChangeDomainlist('whitelist', tags)} - /> -
-
-); +const Domainlist = ({domains, onChangeDomainlist}) => { + return ( +
+

{lang.t('configure.domain-list-title')}

+ +

{lang.t('configure.domain-list-text')}

+ data.split(',').map(d => d.trim())} + onChange={tags => onChangeDomainlist('whitelist', tags)} + /> +
+
+ ); +}; export default Domainlist; - -const lang = new I18n(translations); diff --git a/client/coral-admin/src/containers/Install/InstallContainer.js b/client/coral-admin/src/containers/Install/InstallContainer.js index 123a1493b..75c41387a 100644 --- a/client/coral-admin/src/containers/Install/InstallContainer.js +++ b/client/coral-admin/src/containers/Install/InstallContainer.js @@ -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 { + @@ -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)); diff --git a/client/coral-admin/src/containers/Install/components/Steps/AddOrganizationName.js b/client/coral-admin/src/containers/Install/components/Steps/AddOrganizationName.js index ae23d0048..bcac6ef29 100644 --- a/client/coral-admin/src/containers/Install/components/Steps/AddOrganizationName.js +++ b/client/coral-admin/src/containers/Install/components/Steps/AddOrganizationName.js @@ -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 (
-

- Please tell us the name of your organization. This will appear in emails when - inviting new team members -

+

{lang.t('ADD_ORGANIZATION.DESCRIPTION')}

- +
diff --git a/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js b/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js index cb0f4635f..416fac195 100644 --- a/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js +++ b/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js @@ -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 ? - + : } diff --git a/client/coral-admin/src/containers/Install/components/Steps/FinalStep.js b/client/coral-admin/src/containers/Install/components/Steps/FinalStep.js index e549ebe63..0ad918487 100644 --- a/client/coral-admin/src/containers/Install/components/Steps/FinalStep.js +++ b/client/coral-admin/src/containers/Install/components/Steps/FinalStep.js @@ -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 (
-

- 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. -

- - +

{lang.t('FINAL.DESCRIPTION')}

+ +
); }; diff --git a/client/coral-admin/src/containers/Install/components/Steps/InitialStep.js b/client/coral-admin/src/containers/Install/components/Steps/InitialStep.js index 9f9770fe9..1838e3178 100644 --- a/client/coral-admin/src/containers/Install/components/Steps/InitialStep.js +++ b/client/coral-admin/src/containers/Install/components/Steps/InitialStep.js @@ -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 (
-

- 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. -

- +

{lang.t('INITIAL.DESCRIPTION')}

+
); }; diff --git a/client/coral-admin/src/containers/Install/components/Steps/PermittedDomainsStep.js b/client/coral-admin/src/containers/Install/components/Steps/PermittedDomainsStep.js new file mode 100644 index 000000000..f2c1546f6 --- /dev/null +++ b/client/coral-admin/src/containers/Install/components/Steps/PermittedDomainsStep.js @@ -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 ( +
+

{lang.t('PERMITTED_DOMAINS.TITLE')}

+ +

{lang.t('PERMITTED_DOMAINS.DESCRIPTION')}

+ data.split(',').map(d => d.trim())} + onChange={tags => handleDomainsChange(tags)} + /> +
+ +
+ ); +}; + +export default PermittedDomainsStep; diff --git a/client/coral-admin/src/containers/Install/components/Steps/style.css b/client/coral-admin/src/containers/Install/components/Steps/style.css index a5867e960..dea752298 100644 --- a/client/coral-admin/src/containers/Install/components/Steps/style.css +++ b/client/coral-admin/src/containers/Install/components/Steps/style.css @@ -59,6 +59,12 @@ } } } + + .card { + max-width: 500px; + margin: 20px auto; + text-align: left; + } } .finalStep { diff --git a/client/coral-admin/src/containers/Install/translations.json b/client/coral-admin/src/containers/Install/translations.json new file mode 100644 index 000000000..e5b9f1542 --- /dev/null +++ b/client/coral-admin/src/containers/Install/translations.json @@ -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" + } + } +} diff --git a/client/coral-admin/src/reducers/install.js b/client/coral-admin/src/reducers/install.js index 596fd16cd..1f0f079ff 100644 --- a/client/coral-admin/src/reducers/install.js +++ b/client/coral-admin/src/reducers/install.js @@ -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); diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index b03861384..0303cd4f3 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -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" diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index e52c4aff4..8a81f9fe6 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -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 (

@@ -163,6 +167,7 @@ class Comment extends React.Component { postItem={postItem} depth={depth + 1} asset={asset} + highlighted={highlighted} currentUser={currentUser} postLike={postLike} postFlag={postFlag} diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index ac9c09eac..5526d70e1 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -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 && } {loggedIn && user && } + { + highlightedComment && + + } ({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}) => ({ diff --git a/client/coral-framework/graphql/queries/streamQuery.graphql b/client/coral-framework/graphql/queries/streamQuery.graphql index 0a7f6c549..4f09f628e 100644 --- a/client/coral-framework/graphql/queries/streamQuery.graphql +++ b/client/coral-framework/graphql/queries/streamQuery.graphql @@ -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 diff --git a/client/coral-ui/components/WizardNav.css b/client/coral-ui/components/WizardNav.css index 001ee8603..1f0b78d86 100644 --- a/client/coral-ui/components/WizardNav.css +++ b/client/coral-ui/components/WizardNav.css @@ -13,6 +13,7 @@ position: relative; padding-left: 40px; border-radius: 1px; + min-height: 24px; &:hover { cursor: pointer; diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index 47a8095dc..afa79ea5c 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -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} 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} 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)), diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index de1909df1..2752ad0e3 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -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); }, diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 8800bfebf..bdcdcef78 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -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; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 4dd9a4ba8..92f0d41e2 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -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]