Merge branch 'master' into secrets-fixes

This commit is contained in:
Wyatt Johnson
2017-08-04 10:38:42 +10:00
committed by GitHub
7 changed files with 288 additions and 233 deletions
@@ -1,6 +1,6 @@
import pym from 'coral-framework/services/pym';
import * as actions from '../constants/stream';
import {buildUrl} from 'coral-framework/utils';
import {buildUrl} from 'coral-framework/utils/url';
import queryString from 'query-string';
export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id});
+74
View File
@@ -0,0 +1,74 @@
const DEFAULT_STYLE = {
position: 'fixed',
cursor: 'default',
userSelect: 'none',
backgroundColor: '#323232',
zIndex: 3,
willChange: 'transform, opacity',
transition: 'transform .35s cubic-bezier(.55,0,.1,1), opacity .35s',
pointerEvents: 'none',
padding: '12px 18px',
color: '#fff',
borderRadius: '3px 3px 0 0',
textAlign: 'center',
maxWidth: '400px',
left: '50%',
opacity: 0,
transform: 'translate(-50%, 20px)',
bottom: 0,
boxSizing: 'border-box',
fontFamily: 'Helvetica, "Helvetica Neue", Verdana, sans-serif'
};
export default class Snackbar {
constructor(customStyle = {}) {
this.timeout = null;
this.el = document.createElement('div');
this.el.id = 'coral-notif';
// Apply custom styles to the snackbar.
const style = Object.assign({}, DEFAULT_STYLE, customStyle);
for (let key in style) {
this.el.style[key] = style[key];
}
}
clear() {
this.el.style.opacity = 0;
}
alert(message) {
const [type, text] = message.split('|');
this.el.style.transform = 'translate(-50%, 20px)';
this.el.style.opacity = 0;
this.el.className = `coral-notif-${type}`;
this.el.textContent = text;
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => {
this.el.style.transform = 'translate(-50%, 0)';
this.el.style.opacity = 1;
this.timeout = setTimeout(() => {
this.el.style.opacity = 0;
}, 7000);
}, 0);
}
attach(el, pym) {
el.appendChild(this.el);
// Attach the clear clear notification event to the clear method.
pym.onMessage('coral-clear-notification', this.clear.bind(this));
// Attach the alert to the alert method.
pym.onMessage('coral-alert', this.alert.bind(this));
}
remove() {
this.el.remove();
}
}
+163
View File
@@ -0,0 +1,163 @@
import queryString from 'query-string';
import pym from 'pym.js';
import EventEmitter from 'eventemitter2';
import {buildUrl} from 'coral-framework/utils/url';
import Snackbar from './Snackbar';
const NOTIFICATION_OFFSET = 200;
// Build the URL to load in the pym iframe.
function buildStreamIframeUrl(talkBaseUrl, query) {
let url = [
talkBaseUrl,
talkBaseUrl.match(/\/$/) ? '' : '/', // make sure no double-'/' if opts.talk already ends with '/'
'embed/stream?'
].join('');
url += queryString.stringify(query);
return url;
}
// Get dimensions of viewport.
function viewportDimensions() {
let e = window, a = 'inner';
if (!('innerWidth' in window)) {
a = 'client';
e = document.documentElement || document.body;
}
return {
width: e[`${a}Width`],
height: e[`${a}Height`]
};
}
export default class Stream {
constructor(el, talkBaseUrl, query, opts) {
// Create and save the options.
this.opts = opts;
this.query = query;
this.emitter = new EventEmitter({wildcard: true});
this.pym = new pym.Parent(el.id, buildStreamIframeUrl(talkBaseUrl, query), {
title: opts.title,
id: `${el.id}_iframe`,
name: `${el.id}_iframe`
});
this.snackBar = new Snackbar(opts.snackBarStyles || {});
// Workaround: IOS Safari ignores `width` but respects `min-width` value.
this.pym.el.firstChild.style.width = '1px';
this.pym.el.firstChild.style.minWidth = '100%';
// Resize parent iframe height when child height changes
let cachedHeight;
this.pym.onMessage('height', (height) => {
if (height !== cachedHeight) {
this.pym.el.firstChild.style.height = `${height}px`;
cachedHeight = height;
}
});
// Attach to the events emitted by the pym parent.
if (opts.events) {
opts.events(this.emitter);
}
this.pym.onMessage('getConfig', () => {
this.pym.sendMessage('config', JSON.stringify(opts));
});
// If the auth changes, and someone is listening for it, then re-emit it.
if (opts.onAuthChanged) {
this.pym.onMessage('coral-auth-changed', (message) => {
opts.onAuthChanged(message ? JSON.parse(message) : null);
});
}
// Attach the snackbar to the pym parent and to the body of the page.
this.snackBar.attach(window.document.body, this.pym);
// Remove the permalink comment id from the hash.
this.pym.onMessage('coral-view-all-comments', () => {
const search = queryString.stringify({
...queryString.parse(location.search),
commentId: undefined,
});
// Remove the commentId url param.
const url = buildUrl({...location, search});
// Change the url.
window.history.replaceState({}, document.title, url);
});
// Remove the permalink comment id from the hash.
this.pym.onMessage('coral-view-comment', (id) => {
const search = queryString.stringify({
...queryString.parse(location.search),
commentId: id,
});
// Remove the commentId url param.
const url = buildUrl({...location, search});
// Change the url.
window.history.replaceState({}, document.title, url);
});
// Helps child show notifications at the right scrollTop.
this.pym.onMessage('getPosition', () => {
const {height} = viewportDimensions();
let position = height + document.body.scrollTop;
if (position > NOTIFICATION_OFFSET) {
position = position - NOTIFICATION_OFFSET;
}
this.pym.sendMessage('position', position);
});
// When end-user clicks link in iframe, open it in parent context
this.pym.onMessage('navigate', (url) => {
window.open(url, '_blank').focus();
});
// Pass events from iframe to the event emitter.
this.pym.onMessage('event', (raw) => {
const {eventName, value} = JSON.parse(raw);
this.emitter.emit(eventName, value);
});
// If the user clicks outside the embed, then tell the embed.
document.addEventListener('click', this.handleClick.bind(this), true);
}
login(token) {
this.pym.sendMessage('login', token);
}
logout() {
this.pym.sendMessage('logout');
}
remove() {
// Remove the event listeners.
document.removeEventListener('click', this.handleClick.bind(this));
this.emitter.removeAllListeners();
// Remove the snackbar.
this.snackBar.remove();
// Remove the pym parent.
this.pym.remove();
}
handleClick() {
this.pym.sendMessage('click');
}
}
+21
View File
@@ -0,0 +1,21 @@
export default class StreamInterface {
constructor(stream) {
this._stream = stream;
}
on(eventName, callback) {
return this._stream.emitter.on(eventName, callback);
}
login(token) {
return this._stream.login(token);
}
logout() {
return this._stream.logout();
}
remove() {
return this._stream.remove();
}
}
+20 -224
View File
@@ -1,196 +1,10 @@
import pym from 'pym.js';
import URLSearchParams from 'url-search-params';
import {buildUrl} from 'coral-framework/utils';
import queryString from 'query-string';
import EventEmitter from 'eventemitter2';
// TODO: Styles should live in a separate file
const snackbarStyles = {
position: 'fixed',
cursor: 'default',
userSelect: 'none',
backgroundColor: '#323232',
zIndex: 3,
willChange: 'transform, opacity',
transition: 'transform .35s cubic-bezier(.55,0,.1,1), opacity .35s',
pointerEvents: 'none',
padding: '12px 18px',
color: '#fff',
borderRadius: '3px 3px 0 0',
textAlign: 'center',
maxWidth: '400px',
left: '50%',
opacity: 0,
transform: 'translate(-50%, 20px)',
bottom: 0,
boxSizing: 'border-box',
fontFamily: 'Helvetica, "Helvetica Neue", Verdana, sans-serif'
};
import Stream from './Stream';
import StreamInterface from './StreamInterface';
// This function should return value of window.Coral
const Coral = {};
const Talk = (Coral.Talk = {});
let notificationTimeout = null;
// build the URL to load in the pym iframe
function buildStreamIframeUrl(talkBaseUrl, query) {
let url = [
talkBaseUrl,
talkBaseUrl.match(/\/$/) ? '' : '/', // make sure no double-'/' if opts.talk already ends with '/'
'embed/stream?'
].join('');
url += queryString.stringify(query);
return url;
}
// Set up postMessage listeners/handlers on the pymParent
// e.g. to resize the iframe, and navigate the host page
function configurePymParent(pymParent, eventEmitter, opts) {
let notificationOffset = 200;
let cachedHeight;
const snackbar = document.createElement('div');
// Sends config to pymChild
function sendConfig(config) {
pymParent.sendMessage('config', JSON.stringify(config));
}
if (opts.events) {
opts.events(eventEmitter);
}
pymParent.onMessage('coral-auth-changed', function(message) {
if (opts.onAuthChanged) {
opts.onAuthChanged(message ? JSON.parse(message) : null);
}
});
// Sends config to the child
pymParent.onMessage('getConfig', function() {
sendConfig(opts || {});
});
snackbar.id = 'coral-notif';
for (let key in snackbarStyles) {
snackbar.style[key] = snackbarStyles[key];
}
window.document.body.appendChild(snackbar);
// Notify embed that there was a click outside.
document.addEventListener('click', () => {
pymParent.sendMessage('click');
}, true);
// Workaround: IOS Safari ignores `width` but respects `min-width` value.
pymParent.el.firstChild.style.width = '1px';
pymParent.el.firstChild.style.minWidth = '100%';
// Resize parent iframe height when child height changes
pymParent.onMessage('height', function(height) {
if (height !== cachedHeight) {
pymParent.el.firstChild.style.height = `${height}px`;
cachedHeight = height;
}
});
pymParent.onMessage('coral-clear-notification', function() {
snackbar.style.opacity = 0;
});
// remove the permalink comment id from the hash
pymParent.onMessage('coral-view-all-comments', function() {
const search = queryString.stringify({
...queryString.parse(location.search),
commentId: undefined,
});
// remove the commentId url param
const url = buildUrl({...location, search});
window.history.replaceState(
{},
document.title,
url,
);
});
// remove the permalink comment id from the hash
pymParent.onMessage('coral-view-comment', function(id) {
const search = queryString.stringify({
...queryString.parse(location.search),
commentId: id,
});
// remove the commentId url param
const url = buildUrl({...location, search});
window.history.replaceState(
{},
document.title,
url,
);
});
pymParent.onMessage('coral-alert', function(message) {
const [type, text] = message.split('|');
snackbar.style.transform = 'translate(-50%, 20px)';
snackbar.style.opacity = 0;
snackbar.className = `coral-notif-${type}`;
snackbar.textContent = text;
clearTimeout(notificationTimeout);
notificationTimeout = setTimeout(() => {
snackbar.style.transform = 'translate(-50%, 0)';
snackbar.style.opacity = 1;
notificationTimeout = setTimeout(() => {
snackbar.style.opacity = 0;
}, 7000);
}, 0);
});
// Helps child show notifications at the right scrollTop
pymParent.onMessage('getPosition', function() {
let position = viewport().height + document.body.scrollTop;
if (position > notificationOffset) {
position = position - notificationOffset;
}
pymParent.sendMessage('position', position);
});
// When end-user clicks link in iframe, open it in parent context
pymParent.onMessage('navigate', function(url) {
window.open(url, '_blank').focus();
});
// Pass events from iframe to the event emitter
pymParent.onMessage('event', (raw) => {
const {eventName, value} = JSON.parse(raw);
eventEmitter.emit(eventName, value);
});
// get dimensions of viewport
const viewport = () => {
let e = window, a = 'inner';
if (!('innerWidth' in window)) {
a = 'client';
e = document.documentElement || document.body;
}
return {
width: e[`${a}Width`],
height: e[`${a}Height`]
};
};
}
/**
* Render a Talk stream
@@ -219,17 +33,14 @@ function configurePymParent(pymParent, eventEmitter, opts) {
* });
* ```
*/
Talk.render = function(el, opts) {
Talk.render = (el, opts) => {
if (!el) {
throw new Error(
'Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.'
);
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})`
);
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)
@@ -245,22 +56,27 @@ Talk.render = function(el, opts) {
}
// Compose the query to send down to the Talk API so it knows what to load.
let query = {};
let urlParams = new URLSearchParams(window.location.search);
const query = {};
// Parse the url parameters to extract some of the information.
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('commentId')) {
query.comment_id = urlParams.get('commentId');
}
// Extract the asset id from the options.
if (opts.asset_id) {
query.asset_id = opts.asset_id;
}
// Extract the asset url.
if (opts.asset_url) {
query.asset_url = opts.asset_url;
}
else {
} else if (!opts.asset_id) {
// The asset url was not provided and the asset id was also not provided,
// we need to infer the asset url from details on the page.
try {
query.asset_url = document.querySelector('link[rel="canonical"]').href;
} catch (e) {
@@ -276,31 +92,11 @@ Talk.render = function(el, opts) {
}
}
const pymParent = new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, query), {
title: opts.title,
id: `${el.id}_iframe`,
name: `${el.id}_iframe`
});
// Create the new Stream.
const stream = new Stream(el, opts.talk, query, opts);
const eventEmitter = new EventEmitter({wildcard: true});
configurePymParent(
pymParent,
eventEmitter,
opts
);
return {
on(eventName, callback) {
eventEmitter.on(eventName, callback);
},
login(token) {
pymParent.sendMessage('login', token);
},
logout() {
pymParent.sendMessage('logout');
}
};
// Return the public interface for the stream.
return new StreamInterface(stream);
};
export default Coral;
+1 -8
View File
@@ -169,14 +169,7 @@ export function insertCommentsSorted(nodes, comments, sortOrder = 'CHRONOLOGICAL
export const isTagged = (tags, which) => tags.some((t) => t.tag.name === which);
export function buildUrl({protocol, hostname, port, pathname, search, hash} = window.location) {
if (search && search[0] !== '?') {
search = `?${search}`;
} else if (search === '?') {
search = '';
}
return `${protocol}//${hostname}${port ? `:${port}` : ''}${pathname}${search}${hash}`;
}
export * from './url';
/**
* getSlotFragmentSpreads will return a string in the
+8
View File
@@ -0,0 +1,8 @@
export function buildUrl({protocol, hostname, port, pathname, search, hash} = window.location) {
if (search && search[0] !== '?') {
search = `?${search}`;
} else if (search === '?') {
search = '';
}
return `${protocol}//${hostname}${port ? `:${port}` : ''}${pathname}${search}${hash}`;
}