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'; import { createStorage, connectStorageToPym, } from 'coral-framework/services/storage'; 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, config) { this.query = query; // Extract the non-opts opts from the object. const { events = null, snackBarStyles = null, onAuthChanged = null, ...opts } = config; this.opts = opts; 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(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 (events) { 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 (onAuthChanged) { this.pym.onMessage('coral-auth-changed', message => { 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); // Listens to local storage requests on pym and relay it to local storage. connectStorageToPym( createStorage('localStorage'), this.pym, 'localStorage' ); // Listens to session storage requests on pym and relay it to session storage. connectStorageToPym( createStorage('sessionStorage'), this.pym, 'sessionStorage' ); } enablePluginsDebug() { this.pym.sendMessage('enablePluginsDebug'); } disablePluginsDebug() { this.pym.sendMessage('disablePluginsDebug'); } 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'); } }