diff --git a/README.md b/README.md index 79c523925..274fd7f3d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) -A commenting platform from [The Coral Project](https://coralproject.net). +A commenting platform from [The Coral Project](https://coralproject.net). Talk enters a closed beta in March 2017, but you can download the code for our alpha here. [Read more about Talk here.](https://coralproject.net/products/talk.html) ## Contributing to Talk diff --git a/circle.yml b/circle.yml index f9ac9ea41..bae929df7 100644 --- a/circle.yml +++ b/circle.yml @@ -35,7 +35,7 @@ test: deployment: release: - tag: /[0-9]+(\.[0-9]+)*/ + tag: /v[0-9]+(\.[0-9]+)*/ commands: - bash ./scripts/deploy.sh diff --git a/client/coral-admin/src/containers/Configure/EmbedLink.js b/client/coral-admin/src/containers/Configure/EmbedLink.js index 105b531c5..413f421d7 100644 --- a/client/coral-admin/src/containers/Configure/EmbedLink.js +++ b/client/coral-admin/src/containers/Configure/EmbedLink.js @@ -25,8 +25,24 @@ class EmbedLink extends Component { } render () { - const embedText = `
`; - + const location = window.location; + const talkBaseUrl = [ + location.protocol, + '//', + location.hostname, + location.port ? (`:${ window.location.port}`) : '' + ].join(''); + const coralJsUrl = [talkBaseUrl, '/embed.js'].join(''); + const nonce = String(Math.random()).slice(2); + const streamElementId = `coral_talk_${nonce}`; + const embedText = ` +
+ + `.trim(); return (

{this.props.title}

diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 550fc5fc1..d31b716e9 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -20,6 +20,7 @@ import CommentBox from 'coral-plugin-commentbox/CommentBox'; import UserBox from 'coral-sign-in/components/UserBox'; import SignInContainer from 'coral-sign-in/containers/SignInContainer'; import SuspendedAccount from 'coral-framework/components/SuspendedAccount'; +import ChangeDisplayNameContainer from '../../coral-sign-in/containers/ChangeDisplayNameContainer'; import SettingsContainer from 'coral-settings/containers/SettingsContainer'; import RestrictedContent from 'coral-framework/components/RestrictedContent'; import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer'; @@ -131,6 +132,7 @@ class Embed extends Component { :

{asset.settings.closedMessage}

} {!loggedIn && } + {loggedIn && user && } tag + * (including copypasta dependencies like pym.js), but later there will be a + * build step and this code may use import statements + */ + +// using umd.js (https://github.com/umdjs/umd/blob/master/templates/returnExports.js) +(function (root, factory) { + /* eslint-disable */ + if (typeof define === 'function' && define.amd) { + + // AMD. Register as an anonymous module. + define([], factory); + } else if (typeof module === 'object' && module.exports) { + + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + + // Browser globals (root is window) + root.Coral = factory(); + } + /* eslint-enable */ +}(this, function () { + + // This function should return value of window.Coral + var pym = requirePym(); + var Coral = {}; + var Talk = Coral.Talk = {}; + + /** + * Render a Talk stream + * @param {HTMLElement} el - Element to render the stream in + * @param {Object} opts - Configuration options for talk + * @param {String} opts.talk - Talk base URL + * @param {String} [opts.title] - Title of Stream (rendered in iframe) + * @param {String} [opts.asset] - parent Asset ID or URL. Comments in the + * stream will replies to this asset + */ + Talk.render = function (el, opts) { + if ( ! el) { + throw new Error('Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.'); + } + if (typeof el !== 'object') { + throw new Error('Coral.Talk.render() expected HTMLElement but got ' + el + ' (' + typeof el + ')'); + } + opts = opts || {}; + + // @todo infer this URL without explicit user input (if possible, may have to be added at build/render time of this script) + if (! opts.talk) { + throw new Error('Coral.Talk.render() expects opts.talk as the Talk Base URL'); + } + + // 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 pymParent = new pym.Parent( + el.id, + buildStreamIframeUrl(opts.talk, asset), + { + title: opts.title, + asset_url: asset, + id: el.id + '_iframe', + name: el.id + '_iframe' + } + ); + + configurePymParent(pymParent, asset); + }; + + return Coral; + + // build the URL to load in the pym iframe + function buildStreamIframeUrl(talkBaseUrl, asset) { + var iframeUrl = [ + talkBaseUrl, + (talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/' + 'embed/stream?asset_url=', + encodeURIComponent(asset) + ].join(''); + return iframeUrl; + } + + // Set up postMessage listeners/handlers on the pymParent + // e.g. to resize the iframe, and navigate the host page + function configurePymParent(pymParent, assetUrl) { + var notificationOffset = 200; + var ready = false; + + // Resize parent iframe height when child height changes + pymParent.onMessage('height', function(height) { + + // TODO: In local testing, this is firing nonstop. Maybe there's a bug on the inside? + // Or it's by design of pym... but that's very wasteful of CPU and DOM reflows (jank) + pymParent.el.querySelector('iframe').height = height + 'px'; + }); + + // Helps child show notifications at the right scrollTop + pymParent.onMessage('getPosition', function() { + var position = viewport().height + document.body.scrollTop; + + if (position > notificationOffset) { + position = position - notificationOffset; + } + + pymParent.sendMessage('position', position); + }); + + // Tell child when parent's DOMContentLoaded + pymParent.onMessage('childReady', function () { + var interval = setInterval(function () { + if (ready) { + window.clearInterval(interval); + + // @todo - It's weird to me that this is sent here in addition to the iframe URL. Could it just be in one place? + pymParent.sendMessage('DOMContentLoaded', assetUrl); + } + }, 100); + }); + + // When end-user clicks link in iframe, open it in parent context + pymParent.onMessage('navigate', function (url) { + window.open(url, '_blank').focus(); + }); + + // wait till images and other iframes are loaded before scrolling the page. + // or do we want to be more aggressive and scroll when we hit DOM ready? + document.addEventListener('DOMContentLoaded', function () { + ready = true; + }); + + // get dimensions of viewport + function viewport() { + var e = window, a = 'inner'; + if ( !( 'innerWidth' in window ) ){ + a = 'client'; + e = document.documentElement || document.body; + } + return { + width : e[a + 'Width'], + height : e[a + 'Height'] + }; + } + } + + // return a reference to pym.js + function requirePym() { + var pym; + + // fake AMD `define` so that the pym.js copypasta doesn't create a global + function define(createPym) { + pym = createPym(); + } + define.amd = true; + + /* eslint-disable */ + /*! pym.js - v1.1.2 - 2016-10-25 */ + !function(a){"function"==typeof define&&define.amd?define(a):"undefined"!=typeof module&&module.exports?module.exports=a():window.pym=a.call(this)}(function(){var a="xPYMx",b={},c=function(a){var b=new RegExp("[\\?&]"+a.replace(/[\[]/,"\\[").replace(/[\]]/,"\\]")+"=([^&#]*)"),c=b.exec(location.search);return null===c?"":decodeURIComponent(c[1].replace(/\+/g," "))},d=function(a,b){if("*"===b.xdomain||a.origin.match(new RegExp(b.xdomain+"$")))return!0},e=function(b,c,d){var e=["pym",b,c,d];return e.join(a)},f=function(b){var c=["pym",b,"(\\S+)","(.*)"];return new RegExp("^"+c.join(a)+"$")},g=function(){for(var a=b.autoInitInstances.length,c=a-1;c>=0;c--){var d=b.autoInitInstances[c];d.el.getElementsByTagName("iframe").length&&d.el.getElementsByTagName("iframe")[0].contentWindow||b.autoInitInstances.splice(c,1)}};return b.autoInitInstances=[],b.autoInit=function(){var a=document.querySelectorAll("[data-pym-src]:not([data-pym-auto-initialized])"),c=a.length;g();for(var d=0;d-1&&(b=this.url.substring(c,this.url.length),this.url=this.url.substring(0,c)),this.url.indexOf("?")<0?this.url+="?":this.url+="&",this.iframe.src=this.url+"initialWidth="+a+"&childId="+this.id+"&parentTitle="+encodeURIComponent(document.title)+"&parentUrl="+encodeURIComponent(window.location.href)+b,this.iframe.setAttribute("width","100%"),this.iframe.setAttribute("scrolling","no"),this.iframe.setAttribute("marginheight","0"),this.iframe.setAttribute("frameborder","0"),this.settings.title&&this.iframe.setAttribute("title",this.settings.title),void 0!==this.settings.allowfullscreen&&this.settings.allowfullscreen!==!1&&this.iframe.setAttribute("allowfullscreen",""),void 0!==this.settings.sandbox&&"string"==typeof this.settings.sandbox&&this.iframe.setAttribute("sandbox",this.settings.sandbox),this.settings.id&&(document.getElementById(this.settings.id)||this.iframe.setAttribute("id",this.settings.id)),this.settings.name&&this.iframe.setAttribute("name",this.settings.name);this.el.firstChild;)this.el.removeChild(this.el.firstChild);this.el.appendChild(this.iframe),window.addEventListener("resize",this._onResize)},this._onResize=function(){this.sendWidth()}.bind(this),this._fire=function(a,b){if(a in this.messageHandlers)for(var c=0;c ({type: actions.SHOW_SIGNIN_DIALOG, offset}); export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG}); +export const createDisplayNameRequest = () => ({type: actions.CREATE_DISPLAYNAME_REQUEST}); +export const showCreateDisplayNameDialog = () => ({type: actions.SHOW_CREATEDISPLAYNAME_DIALOG}); +export const hideCreateDisplayNameDialog = () => ({type: actions.HIDE_CREATEDISPLAYNAME_DIALOG}); + +const createDisplayNameSuccess = () => ({type: actions.CREATEDISPLAYNAME_SUCCESS}); +const createDisplayNameFailure = error => ({type: actions.CREATEDISPLAYNAME_FAILURE, error}); + +export const updateDisplayName = displayName => ({type: actions.UPDATE_DISPLAYNAME, displayName}); + +export const createDisplayName = (userId, formData) => dispatch => { + dispatch(createDisplayNameRequest()); + coralApi(`/users/${userId}/displayname`, {method: 'POST', body: formData}) + .then((user) => { + dispatch(createDisplayNameSuccess()); + dispatch(hideCreateDisplayNameDialog()); + dispatch(updateDisplayName(user)); + }) + .catch(error => { + dispatch(createDisplayNameFailure(lang.t(`error.${error.message}`))); + }); +}; + export const changeView = view => dispatch => dispatch({ type: actions.CHANGE_VIEW, @@ -60,6 +82,19 @@ export const fetchSignInFacebook = () => dispatch => { ); }; +// Sign Up Facebook + +const signUpFacebookRequest = () => ({type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST}); + +export const fetchSignUpFacebook = () => dispatch => { + dispatch(signUpFacebookRequest()); + window.open( + `${base}/auth/facebook`, + 'Continue with Facebook', + 'menubar=0,resizable=0,width=500,height=500,top=200,left=500' + ); +}; + export const facebookCallback = (err, data) => dispatch => { if (err) { signInFacebookFailure(err); @@ -69,6 +104,7 @@ export const facebookCallback = (err, data) => dispatch => { const user = JSON.parse(data); dispatch(signInFacebookSuccess(user)); dispatch(hideSignInDialog()); + dispatch(showCreateDisplayNameDialog()); } catch (err) { dispatch(signInFacebookFailure(err)); return; diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index 1dae348df..2b5f762c1 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -4,6 +4,13 @@ export const CLEAN_STATE = 'CLEAN_STATE'; export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG'; export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG'; +export const CREATE_DISPLAYNAME_REQUEST = 'CREATE_DISPLAYNAME_REQUEST'; +export const CREATEDISPLAYNAME_SUCCESS = 'CREATEDISPLAYNAME_SUCCESS'; +export const CREATEDISPLAYNAME_FAILURE = 'CREATEDISPLAYNAME_FAILURE'; +export const CREATE_DISPLAYNAME = 'CREATE_DISPLAYNAME'; +export const SHOW_CREATEDISPLAYNAME_DIALOG = 'SHOW_CREATEDISPLAYNAME_DIALOG'; +export const HIDE_CREATEDISPLAYNAME_DIALOG = 'HIDE_CREATEDISPLAYNAME_DIALOG'; + export const FETCH_SIGNUP_REQUEST = 'FETCH_SIGNUP_REQUEST'; export const FETCH_SIGNUP_FAILURE = 'FETCH_SIGNUP_FAILURE'; export const FETCH_SIGNUP_SUCCESS = 'FETCH_SIGNUP_SUCCESS'; @@ -16,6 +23,7 @@ export const FETCH_SIGNIN_FACEBOOK_REQUEST = 'FETCH_SIGNIN_FACEBOOK_REQUEST'; export const FETCH_SIGNIN_FACEBOOK_FAILURE = 'FETCH_SIGNIN_FACEBOOK_FAILURE'; export const FETCH_SIGNIN_FACEBOOK_SUCCESS = 'FETCH_SIGNIN_FACEBOOK_SUCCESS'; +export const FETCH_SIGNUP_FACEBOOK_REQUEST = 'FETCH_SIGNUP_FACEBOOK_REQUEST'; export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST'; export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS'; export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE'; @@ -33,6 +41,7 @@ export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE'; export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN'; +export const UPDATE_DISPLAYNAME = 'UPDATE_DISPLAYNAME'; export const EMAIL_CONFIRM_ERROR = 'EMAIL_CONFIRM_ERROR'; export const CONFIRM_EMAIL_REQUEST = 'CONFIRM_EMAIL_REQUEST'; export const CONFIRM_EMAIL_SUCCESS = 'CONFIRM_EMAIL_SUCCESS'; diff --git a/client/coral-framework/constants/user.js b/client/coral-framework/constants/user.js index 9c6f508fe..57f4e706b 100644 --- a/client/coral-framework/constants/user.js +++ b/client/coral-framework/constants/user.js @@ -5,3 +5,4 @@ export const COMMENTS_BY_USER_REQUEST = 'COMMENTS_BY_USER_REQUEST'; export const COMMENTS_BY_USER_SUCCESS = 'COMMENTS_BY_USER_SUCCESS'; export const COMMENTS_BY_USER_FAILURE = 'COMMENTS_BY_USER_FAILURE'; export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; +export const UPDATE_DISPLAYNAME = 'UPDATE_DISPLAYNAME'; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 6cb4e97cd..2da589507 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -7,6 +7,7 @@ const initialState = Map({ isAdmin: false, user: null, showSignInDialog: false, + showCreateDisplayNameDialog: false, view: 'SIGNIN', error: '', passwordRequestSuccess: null, @@ -14,7 +15,8 @@ const initialState = Map({ emailConfirmationFailure: false, emailConfirmationLoading: false, emailConfirmationSuccess: false, - successSignUp: false + successSignUp: false, + fromSignUp: false }); const purge = user => { @@ -41,6 +43,21 @@ export default function auth (state = initialState, action) { emailConfirmationLoading: false, successSignUp: false })); + case actions.SHOW_CREATEDISPLAYNAME_DIALOG : + return state + .set('showCreateDisplayNameDialog', true); + case actions.HIDE_CREATEDISPLAYNAME_DIALOG : + return state.merge(Map({ + showCreateDisplayNameDialog: false + })); + case actions.CREATEDISPLAYNAME_SUCCESS : + return state.merge(Map({ + showCreateDisplayNameDialog: false, + error: '' + })); + case actions.CREATEDISPLAYNAME_FAILURE : + return state + .set('error', action.error); case actions.CHANGE_VIEW : return state .set('error', '') @@ -72,6 +89,12 @@ export default function auth (state = initialState, action) { .set('isLoading', false) .set('error', action.error) .set('user', null); + case actions.FETCH_SIGNUP_FACEBOOK_REQUEST: + return state + .set('fromSignUp', true); + case actions.FETCH_SIGNIN_FACEBOOK_REQUEST: + return state + .set('fromSignUp', false); case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS: return state .set('user', purge(action.user)) @@ -107,6 +130,9 @@ export default function auth (state = initialState, action) { return state .set('passwordRequestFailure', 'There was an error sending your password reset email. Please try again soon!') .set('passwordRequestSuccess', null); + case actions.UPDATE_DISPLAYNAME: + return state + .set('user', purge(action.displayName)); case actions.EMAIL_CONFIRM_ERROR: return state .set('emailConfirmationFailure', true) diff --git a/client/coral-sign-in/components/CreateDisplayNameDialog.js b/client/coral-sign-in/components/CreateDisplayNameDialog.js new file mode 100644 index 000000000..4d8ef9fb3 --- /dev/null +++ b/client/coral-sign-in/components/CreateDisplayNameDialog.js @@ -0,0 +1,48 @@ +import React from 'react'; +import FormField from 'coral-ui/components/FormField'; +import Alert from './Alert'; +import Button from 'coral-ui/components/Button'; +import {Dialog} from 'coral-ui'; +import styles from './styles.css'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations'; +const lang = new I18n(translations); + +const CreateDisplayNameDialog = ({open, handleClose, offset, formData, handleSubmitDisplayName, handleChange, ...props}) => ( + + × +
+
+

+ {lang.t('createdisplay.writeyourusername')} +

+
+
+ + { props.auth.error && {props.auth.error} } +
+ + { props.errors.displayName && {lang.t('createdisplay.specialCharacters')} } +
+ +
+ +
+
+
+); + +export default CreateDisplayNameDialog; diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index 246d9d884..80a6fcde4 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -14,7 +14,7 @@ const SignUpContent = ({handleChange, formData, ...props}) => (
-
diff --git a/client/coral-sign-in/containers/ChangeDisplayNameContainer.js b/client/coral-sign-in/containers/ChangeDisplayNameContainer.js new file mode 100644 index 000000000..bc7fe8267 --- /dev/null +++ b/client/coral-sign-in/containers/ChangeDisplayNameContainer.js @@ -0,0 +1,135 @@ +import React, {Component} from 'react'; +import {connect} from 'react-redux'; + +import validate from 'coral-framework/helpers/validate'; +import errorMsj from 'coral-framework/helpers/error'; + +import CreateDisplayNameDialog from '../components/CreateDisplayNameDialog'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations'; +const lang = new I18n(translations); + +import { + showCreateDisplayNameDialog, + hideCreateDisplayNameDialog, + invalidForm, + validForm, + createDisplayName +} from '../../coral-framework/actions/auth'; + +class ChangeDisplayNameContainer extends Component { + initialState = { + formData: { + displayName: '', + }, + errors: {}, + showErrors: false + }; + + constructor(props) { + super(props); + this.state = this.initialState; + this.handleChange = this.handleChange.bind(this); + this.handleSubmitDisplayName = this.handleSubmitDisplayName.bind(this); + this.handleClose = this.handleClose.bind(this); + this.addError = this.addError.bind(this); + } + + handleChange(e) { + const {name, value} = e.target; + this.setState(state => ({ + ...state, + formData: { + ...state.formData, + [name]: value + } + }), () => { + this.validation(name, value); + }); + } + + addError(name, error) { + return this.setState(state => ({ + errors: { + ...state.errors, + [name]: error + } + })); + } + + validation(name, value) { + const {addError} = this; + + if (!value.length) { + addError(name, lang.t('createdisplay.requiredField')); + } else if (!validate[name](value)) { + addError(name, errorMsj[name]); + } else { + const { [name]: prop, ...errors } = this.state.errors; // eslint-disable-line + // Removes Error + this.setState(state => ({...state, errors})); + } + } + + isCompleted() { + const {formData} = this.state; + return !Object.keys(formData).filter(prop => !formData[prop].length).length; + } + + displayErrors(show = true) { + this.setState({showErrors: show}); + } + + handleSubmitDisplayName(e) { + e.preventDefault(); + const {errors} = this.state; + const {validForm, invalidForm} = this.props; + this.displayErrors(); + if (this.isCompleted() && !Object.keys(errors).length) { + this.props.createDisplayName(this.props.user.id, this.state.formData); + validForm(); + } else { + invalidForm(lang.t('createdisplay.checkTheForm')); + } + } + + handleClose() { + this.props.hideCreateDisplayNameDialog(); + } + + render() { + const {loggedIn, auth, offset} = this.props; + return ( +
+ +
+ ); + } +} + +const mapStateToProps = state => ({ + auth: state.auth.toJS() +}); + +const mapDispatchToProps = dispatch => ({ + createDisplayName: (userid, formData) => dispatch(createDisplayName(userid, formData)), + showCreateDisplayNameDialog: () => dispatch(showCreateDisplayNameDialog()), + hideCreateDisplayNameDialog: () => dispatch(hideCreateDisplayNameDialog()), + invalidForm: error => dispatch(invalidForm(error)), + validForm: () => dispatch(validForm()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ChangeDisplayNameContainer); diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 2435cdcfd..937c830c9 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -15,6 +15,7 @@ import { showSignInDialog, hideSignInDialog, fetchSignInFacebook, + fetchSignUpFacebook, fetchForgotPassword, requestConfirmEmail, facebookCallback, @@ -177,6 +178,7 @@ const mapDispatchToProps = dispatch => ({ fetchSignUp: formData => dispatch(fetchSignUp(formData)), fetchSignIn: formData => dispatch(fetchSignIn(formData)), fetchSignInFacebook: () => dispatch(fetchSignInFacebook()), + fetchSignUpFacebook: () => dispatch(fetchSignUpFacebook()), fetchForgotPassword: formData => dispatch(fetchForgotPassword(formData)), requestConfirmEmail: email => dispatch(requestConfirmEmail(email)), showSignInDialog: () => dispatch(showSignInDialog()), diff --git a/client/coral-sign-in/translations.js b/client/coral-sign-in/translations.js index 137141d34..5751370a6 100644 --- a/client/coral-sign-in/translations.js +++ b/client/coral-sign-in/translations.js @@ -26,7 +26,17 @@ export default { passwordsDontMatch: 'Passwords don\'t match.', specialCharacters: 'Display names can contain letters, numbers and _ only', checkTheForm: 'Invalid Form. Please, check the fields' - } + }, + 'createdisplay': { + writeyourusername: 'Write your username', + yourusername: 'Your username is publicly visible on all comments you post. A username is needed before you can post your first comment.', + displayName: 'Display Name', + save: 'Save', + requiredField: 'Required field', + errorCreate: 'Error when changing display name', + checkTheForm: 'Invalid Form. Please, check the fields', + specialCharacters: 'Display names can contain letters, numbers and _ only' + }, }, es: { 'signIn': { @@ -55,6 +65,16 @@ export default { passwordsDontMatch: 'Las contraseñas no coinciden', specialCharacters: 'Los nombres pueden contener letras, números y _', checkTheForm: 'Formulario Inválido. Por favor, completa los campos' - } + }, + 'createdisplay': { + writeyourusername: 'Escribe tu nombre', + yourusername: 'Tu nombre es visible publicamente en todos los comentarios que publiques. Es necesario tener un nombre de usuario antes de poder publicar tu primer comentario.', + displayName: 'Nombre a mostrar', + save: 'Guardar', + requiredField: 'Campo necesario', + errorCreate: 'Hubo un error al cambiar el nombre de usuario', + checkTheForm: 'Formulario Invalido. Por favor, verifica los campos', + specialCharacters: 'Sólo pueden contener letras, números y _' + }, } }; diff --git a/routes/api/users/index.js b/routes/api/users/index.js index 6da7b9a4b..1c7bc7bb2 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -60,6 +60,14 @@ router.post('/:user_id/status', authorization.needed('ADMIN'), (req, res, next) .catch(next); }); +router.post('/:user_id/displayname', authorization.needed(), (req, res, next) => { + UsersService.setDisplayName(req.params.user_id, req.body.displayName) + .then((user) => { + res.status(201).json(user); + }) + .catch(next); +}); + router.post('/:user_id/email', authorization.needed('admin'), (req, res, next) => { UsersService.findById(req.params.user_id) .then(user => { diff --git a/routes/index.js b/routes/index.js index a1c18bbe2..add288b10 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,9 +1,11 @@ const express = require('express'); +const path = require('path'); const router = express.Router(); router.use('/api/v1', require('./api')); router.use('/admin', require('./admin')); router.use('/embed', require('./embed')); +router.get('/embed.js', (req, res) => res.sendFile(path.join(__dirname, '../client/coral-embed/index.js'))); router.use('/assets', require('./assets')); router.get('/', (req, res) => { diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 51336b44d..fd681fc7f 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -8,11 +8,11 @@ docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS deploy_tag() { # Find our individual versions from the tags - if [ -n "$(echo $CIRCLE_TAG | grep -E '.*\..*\..*')" ] + if [ -n "$(echo $CIRCLE_TAG | grep -E 'v.*\..*\..*')" ] then - major=$(echo $CIRCLE_TAG | cut -d. -f1) - minor=$(echo $CIRCLE_TAG | cut -d. -f2) - patch=$(echo $CIRCLE_TAG | cut -d. -f3) + major=$(echo ${CIRCLE_TAG//v} | cut -d. -f1) + minor=$(echo ${CIRCLE_TAG//v} | cut -d. -f2) + patch=$(echo ${CIRCLE_TAG//v} | cut -d. -f3) major_version_tag=$major minor_version_tag=$major.$minor diff --git a/services/users.js b/services/users.js index 70472bea7..fd39a50c1 100644 --- a/services/users.js +++ b/services/users.js @@ -353,6 +353,25 @@ module.exports = class UsersService { return UserModel.update({id}, {$set: {status}}); } + /** + * Set the display name of a user. + * @param {String} id id of a user + * @param {String} displayName display name to set + * @param {Function} done callback after the operation is complete + */ + static setDisplayName(id, displayName) { + + return UsersService.isValidDisplayName(displayName) + .then(() => { // displayName is valid + return UserModel.update( + {id}, + {$set: {'displayName': displayName}}) + .then(() => { + return UserModel.findOne({'id': id}); + }); + }); + } + /** * Finds a user with the id. * @param {String} id user id (uuid) diff --git a/test/e2e/pages/embedStreamPage.js b/test/e2e/pages/embedStreamPage.js index bdf2f9440..372b94b66 100644 --- a/test/e2e/pages/embedStreamPage.js +++ b/test/e2e/pages/embedStreamPage.js @@ -6,8 +6,8 @@ const embedStreamCommands = { ready() { return this .waitForElementVisible('body', 4000) - .waitForElementVisible('iframe#coralStreamIframe') - .api.frame('coralStreamIframe'); + .waitForElementVisible('#coralStreamEmbed > iframe') + .api.frame('coralStreamEmbed_iframe'); }, signUp(user) { return this diff --git a/test/e2e/tests/EmbedStreamTests.js b/test/e2e/tests/EmbedStreamTests.js index 7343c3d0a..1b1fb40ab 100644 --- a/test/e2e/tests/EmbedStreamTests.js +++ b/test/e2e/tests/EmbedStreamTests.js @@ -26,7 +26,7 @@ module.exports = { // Load Page client.resizeWindow(1200, 800) .url(client.globals.baseUrl) - .frame('coralStreamIframe') + .frame('coralStreamEmbed_iframe') // Register and Log In .waitForElementVisible('#coralSignInButton', 2000) @@ -65,7 +65,7 @@ module.exports = { // Load Page client.url(client.globals.baseUrl) - .frame('coralStreamIframe'); + .frame('coralStreamEmbed_iframe'); // Post a comment client.waitForElementVisible('.coral-plugin-commentbox-button', 2000) @@ -91,7 +91,7 @@ module.exports = { // Load Page client.resizeWindow(1200, 800) .url(client.globals.baseUrl) - .frame('coralStreamIframe'); + .frame('coralStreamEmbed_iframe'); // Post a comment client.waitForElementVisible('.coral-plugin-commentbox-button', 2000) @@ -138,7 +138,7 @@ module.exports = { // Load Page client.resizeWindow(1200, 800) .url(client.globals.baseUrl) - .frame('coralStreamIframe'); + .frame('coralStreamEmbed_iframe'); // Post a reply client.waitForElementVisible('.coral-plugin-replies-reply-button', 5000) @@ -161,7 +161,7 @@ module.exports = { 'Total comment count premod on': client => { client.perform((client, done) => { client.url(client.globals.baseUrl) - .frame('coralStreamIframe'); + .frame('coralStreamEmbed_iframe'); // Verify that comment count is correct client.waitForElementVisible('.coral-plugin-comment-count-text', 2000) diff --git a/test/services/users.js b/test/services/users.js index e2e5655e4..ba2bedcc3 100644 --- a/test/services/users.js +++ b/test/services/users.js @@ -178,6 +178,25 @@ describe('services.UsersService', () => { }); }); + describe('#setDisplayName', () => { + it('should set the display name to a new unique one', () => { + return UsersService + .setDisplayName(mockUsers[0].id, 'maria') + .then(() => UsersService.findById(mockUsers[0].id)) + .then((user) => { + expect(user).to.have.property('displayName', 'maria'); + }); + }); + + it('should return an error when the displayName is not unique', () => { + return UsersService + .setDisplayName(mockUsers[0].id, 'marvel') + .catch((error) => { + expect(error).to.not.be.null; + }); + }); + }); + describe('#ban', () => { it('should set the status to banned', () => { return UsersService diff --git a/views/article.ejs b/views/article.ejs index 7382f6c35..72fc69ce3 100644 --- a/views/article.ejs +++ b/views/article.ejs @@ -23,72 +23,12 @@

<%= body %>

Admin - All Assets

+ - - -