mirror of
https://github.com/wassname/talk.git
synced 2026-10-01 12:42:56 +08:00
Merge branch 'master' into circle-release-fix
This commit is contained in:
commit
bc14a9a758
19 files changed
+555
-79
No files matched your search
@@ -25,8 +25,24 @@ class EmbedLink extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const embedText = `<div id='coralStreamEmbed'></div><script type='text/javascript' src='${window.location.protocol}//pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embed/stream', {title: 'Comments'});</script>`;
|
||||
|
||||
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 = `
|
||||
<div id="${streamElementId}"></div>
|
||||
<script src="${coralJsUrl}" async onload="
|
||||
Coral.Talk.render(document.getElementById('${streamElementId}'), {
|
||||
talk: '${talkBaseUrl}'
|
||||
});
|
||||
"></script>
|
||||
`.trim();
|
||||
return (
|
||||
<div>
|
||||
<h3>{this.props.title}</h3>
|
||||
|
||||
@@ -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 {
|
||||
: <p>{asset.settings.closedMessage}</p>
|
||||
}
|
||||
{!loggedIn && <SignInContainer offset={signInOffset}/>}
|
||||
{loggedIn && user && <ChangeDisplayNameContainer loggedIn={loggedIn} offset={signInOffset} user={user} />}
|
||||
<Stream
|
||||
refetch={refetch}
|
||||
addNotification={this.props.addNotification}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/* eslint-disable no-var, prefer-template */
|
||||
/**
|
||||
* @file Exposes the main 'window.Coral.Talk' API that developers can use to
|
||||
* render Talk streams in their webpages
|
||||
* @todo Currently implemented to be included directly via <script> 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<c;++d){var e=a[d];e.setAttribute("data-pym-auto-initialized",""),""===e.id&&(e.id="pym-"+d);var f=e.getAttribute("data-pym-src"),h={xdomain:"string",title:"string",name:"string",id:"string",sandbox:"string",allowfullscreen:"boolean"},i={};for(var j in h)if(null!==e.getAttribute("data-pym-"+j))switch(h[j]){case"boolean":i[j]=!("false"===e.getAttribute("data-pym-"+j));break;case"string":i[j]=e.getAttribute("data-pym-"+j);break;default:console.err("unrecognized attribute type")}var k=new b.Parent(e.id,f,i);b.autoInitInstances.push(k)}return b.autoInitInstances},b.Parent=function(a,b,c){this.id=a,this.url=b,this.el=document.getElementById(a),this.iframe=null,this.settings={xdomain:"*"},this.messageRegex=f(this.id),this.messageHandlers={},c=c||{},this._constructIframe=function(){var a=this.el.offsetWidth.toString();this.iframe=document.createElement("iframe");var b="",c=this.url.indexOf("#");for(c>-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<this.messageHandlers[a].length;c++)this.messageHandlers[a][c].call(this,b)},this.remove=function(){window.removeEventListener("message",this._processMessage),window.removeEventListener("resize",this._onResize),this.el.removeChild(this.iframe),g()},this._processMessage=function(a){if(d(a,this.settings)&&"string"==typeof a.data){var b=a.data.match(this.messageRegex);if(!b||3!==b.length)return!1;var c=b[1],e=b[2];this._fire(c,e)}}.bind(this),this._onHeightMessage=function(a){var b=parseInt(a);this.iframe.setAttribute("height",b+"px")},this._onNavigateToMessage=function(a){document.location.href=a},this._onScrollToChildPosMessage=function(a){var b=document.getElementById(this.id).getBoundingClientRect().top+window.pageYOffset,c=b+parseInt(a);window.scrollTo(0,c)},this.onMessage=function(a,b){a in this.messageHandlers||(this.messageHandlers[a]=[]),this.messageHandlers[a].push(b)},this.sendMessage=function(a,b){this.el.getElementsByTagName("iframe").length&&(this.el.getElementsByTagName("iframe")[0].contentWindow?this.el.getElementsByTagName("iframe")[0].contentWindow.postMessage(e(this.id,a,b),"*"):this.remove())},this.sendWidth=function(){var a=this.el.offsetWidth.toString();this.sendMessage("width",a)};for(var h in c)this.settings[h]=c[h];return this.onMessage("height",this._onHeightMessage),this.onMessage("navigateTo",this._onNavigateToMessage),this.onMessage("scrollToChildPos",this._onScrollToChildPosMessage),window.addEventListener("message",this._processMessage,!1),this._constructIframe(),this},b.Child=function(b){this.parentWidth=null,this.id=null,this.parentTitle=null,this.parentUrl=null,this.settings={renderCallback:null,xdomain:"*",polling:0},this.timerId=null,this.messageRegex=null,this.messageHandlers={},b=b||{},this.onMessage=function(a,b){a in this.messageHandlers||(this.messageHandlers[a]=[]),this.messageHandlLine truncated
|
||||
/* eslint-enable */
|
||||
return pym;
|
||||
}
|
||||
}));
|
||||
|
||||
/*
|
||||
pym.js LICENSE (https://github.com/nprapps/pym.js/blob/master/LICENSE)
|
||||
Copyright (c) 2014 NPR
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
@@ -8,6 +8,28 @@ import coralApi, {base} from '../helpers/response';
|
||||
export const showSignInDialog = (offset = 0) => ({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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
@@ -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)
|
||||
|
||||
@@ -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}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
id="createDisplayNameDialog"
|
||||
open={open}
|
||||
style={{
|
||||
position: 'relative',
|
||||
top: offset !== 0 && offset
|
||||
}}>
|
||||
<span className={styles.close} onClick={handleClose}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h1>
|
||||
{lang.t('createdisplay.writeyourusername')}
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="displayName">{lang.t('createdisplay.yourusername')}</label>
|
||||
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
|
||||
<form id="saveDisplayName" onSubmit={handleSubmitDisplayName}>
|
||||
<FormField
|
||||
id="displayName"
|
||||
type="string"
|
||||
label={lang.t('createdisplay.displayName')}
|
||||
value={formData.displayName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{ props.errors.displayName && <span className={styles.hint}> {lang.t('createdisplay.specialCharacters')} </span> }
|
||||
<div className={styles.action}>
|
||||
<Button id="save" type="submit" className={styles.saveButton}>{lang.t('createdisplay.save')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
export default CreateDisplayNameDialog;
|
||||
@@ -14,7 +14,7 @@ const SignUpContent = ({handleChange, formData, ...props}) => (
|
||||
</h1>
|
||||
</div>
|
||||
<div className={styles.socialConnections}>
|
||||
<Button cStyle="facebook" onClick={props.fetchSignInFacebook} full>
|
||||
<Button cStyle="facebook" onClick={props.fetchSignUpFacebook} full>
|
||||
{lang.t('signIn.facebookSignUp')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<CreateDisplayNameDialog
|
||||
open={auth.showCreateDisplayNameDialog && auth.fromSignUp}
|
||||
offset={offset}
|
||||
handleClose={this.handleClose}
|
||||
loggedIn={loggedIn}
|
||||
handleSubmitDisplayName={this.handleSubmitDisplayName}
|
||||
{...this}
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -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()),
|
||||
|
||||
@@ -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 _'
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
-66
@@ -23,72 +23,12 @@
|
||||
<p><%= body %></p>
|
||||
<p><a href="/admin">Admin</a> - <a href="/assets">All Assets</a></p>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
<script src="/embed.js" async onload="
|
||||
Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
|
||||
talk: '/',
|
||||
asset: <%= asset_url ? asset_url : 'undefined' %>
|
||||
})
|
||||
"></script>
|
||||
</main>
|
||||
|
||||
<script type='text/javascript' src='<%= basePath %>/pym.v1.min.js'></script>
|
||||
<script>
|
||||
var ready = false;
|
||||
var notificationOffset = 200;
|
||||
|
||||
// default to using the window.location
|
||||
var url = window.location.protocol + '//' + window.location.host + window.location.pathname;
|
||||
|
||||
// if a url is passed into the template prefer it to the current url
|
||||
<%if (asset_url.length > 0) { %>
|
||||
url = '<%= asset_url %>';
|
||||
<%}%>
|
||||
|
||||
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream?asset_url=' + encodeURIComponent(url), {title: 'Talk Comments', id:'coralStreamIframe', name: 'coralStreamIframe', asset_url: url});
|
||||
pymParent.onMessage('height', function(height) {
|
||||
document.querySelector('#coralStreamEmbed iframe').height = height + 'px';
|
||||
})
|
||||
|
||||
|
||||
pymParent.onMessage('getPosition', function(notification) {
|
||||
var position = viewport().height + document.body.scrollTop;
|
||||
|
||||
if (position > notificationOffset) {
|
||||
position = position - notificationOffset;
|
||||
}
|
||||
|
||||
pymParent.sendMessage('position', position);
|
||||
});
|
||||
|
||||
pymParent.onMessage('childReady', function () {
|
||||
var interval = setInterval(function () {
|
||||
if (ready) {
|
||||
window.clearInterval(interval);
|
||||
|
||||
// default to using the window.location
|
||||
var url = window.location.hash;
|
||||
|
||||
// if a url is passed into the template prefer it to the current url
|
||||
<%if (asset_url.length > 0) { %>
|
||||
url = '<%= asset_url %>';
|
||||
<%}%>
|
||||
|
||||
pymParent.sendMessage('DOMContentLoaded', url);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
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;
|
||||
});
|
||||
|
||||
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' ] }
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user