diff --git a/Dockerfile.onbuild b/Dockerfile.onbuild index 8fe4cd824..ac88bd509 100644 --- a/Dockerfile.onbuild +++ b/Dockerfile.onbuild @@ -11,6 +11,7 @@ ONBUILD ARG TALK_DEFAULT_LANG=en ONBUILD ARG TALK_WHITELISTED_LANGUAGES ONBUILD ARG TALK_PLUGINS_JSON ONBUILD ARG TALK_WEBPACK_SOURCE_MAP +ONBUILD ARG TALK_DEFAULT_LAZY_RENDER # Bundle app source ONBUILD COPY . /usr/src/app diff --git a/client/coral-embed/src/Stream.js b/client/coral-embed/src/Stream.js index f17693870..7b882f0fa 100644 --- a/client/coral-embed/src/Stream.js +++ b/client/coral-embed/src/Stream.js @@ -3,6 +3,7 @@ import pym from 'pym.js'; import EventEmitter from 'eventemitter2'; import { buildUrl } from 'coral-framework/utils/url'; import Snackbar from './Snackbar'; +import onIntersect from './onIntersect'; import { createStorage, connectStorageToPym, @@ -50,15 +51,62 @@ export default class Stream { ...opts } = config; + this.onAuthChanged = onAuthChanged; + this.el = el; + this.talkBaseUrl = talkBaseUrl; 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 || {}); + this.emitter = new EventEmitter({ wildcard: true }); + // Attach to the events emitted by the pym parent. + if (events) { + events(this.emitter); + } + if (config.lazy || process.env.TALK_DEFAULT_LAZY_RENDER === 'TRUE') { + const renderOnIntersect = () => onIntersect(this.el, () => this.render()); + if (!window.IntersectionObserver) { + // Include a polyfill for the intersection observer. + import('intersection-observer') + .then(() => { + // Polyfill applied. + renderOnIntersect(); + }) + .catch(e => { + console.error(e); + // Loading polyfill failed, just render it directly. + this.render(); + }); + } else { + // No need for polyfill. + renderOnIntersect(); + } + } else { + this.render(); + } + } + + assertRendered() { + if (!this.pym) { + throw new Error('Stream Embed must be rendered first'); + } + } + + isRendered() { + return !!this.pym; + } + + render() { + if (this.pym) { + throw new Error('Stream Embed already rendered'); + } + this.pym = new pym.Parent( + this.el.id, + buildStreamIframeUrl(this.talkBaseUrl, this.query), + { + title: this.opts.title, + id: `${this.el.id}_iframe`, + name: `${this.el.id}_iframe`, + } + ); // Workaround: IOS Safari ignores `width` but respects `min-width` value. this.pym.el.firstChild.style.width = '1px'; @@ -73,19 +121,14 @@ export default class Stream { } }); - // 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)); + this.pym.sendMessage('config', JSON.stringify(this.opts)); }); // If the auth changes, and someone is listening for it, then re-emit it. - if (onAuthChanged) { + if (this.onAuthChanged) { this.pym.onMessage('coral-auth-changed', message => { - onAuthChanged(message ? JSON.parse(message) : null); + this.onAuthChanged(message ? JSON.parse(message) : null); }); } @@ -163,22 +206,27 @@ export default class Stream { } enablePluginsDebug() { + this.assertRendered(); this.pym.sendMessage('enablePluginsDebug'); } disablePluginsDebug() { + this.assertRendered(); this.pym.sendMessage('disablePluginsDebug'); } login(token) { + this.assertRendered(); this.pym.sendMessage('login', token); } logout() { + this.assertRendered(); this.pym.sendMessage('logout'); } remove() { + this.assertRendered(); // Remove the event listeners. document.removeEventListener('click', this.handleClick.bind(this)); this.emitter.removeAllListeners(); @@ -191,6 +239,7 @@ export default class Stream { } handleClick() { + this.assertRendered(); this.pym.sendMessage('click'); } } diff --git a/client/coral-embed/src/index.js b/client/coral-embed/src/index.js index 2f456c4e2..17afd4c92 100644 --- a/client/coral-embed/src/index.js +++ b/client/coral-embed/src/index.js @@ -34,6 +34,7 @@ export class Talk { * @param {String} [config.asset_url] - Asset URL * @param {String} [config.asset_id] - Asset ID * @param {String} [config.auth_token] - (optional) A jwt representing the session + * @param {String} [config.lazy] - (optional) If set the stream will only render lazily. * @return {Object} * * Example: diff --git a/client/coral-embed/src/onIntersect.js b/client/coral-embed/src/onIntersect.js new file mode 100644 index 000000000..9814c178c --- /dev/null +++ b/client/coral-embed/src/onIntersect.js @@ -0,0 +1,20 @@ +export default function onIntersect(el, callback) { + if (!IntersectionObserver) { + // tslint:disable-next-line:no-console + console.warn('IntersectionObserver not available'); + callback(); + return; + } + const options = { + rootMargin: '100px', + threshold: 1.0, + }; + + const observer = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + observer.disconnect(); + callback(); + } + }, options); + observer.observe(el); +} diff --git a/package.json b/package.json index e4b6088c3..3c341fe2c 100644 --- a/package.json +++ b/package.json @@ -237,6 +237,7 @@ "eslint-plugin-mocha": "^4.11.0", "husky": "^0.14.3", "identity-obj-proxy": "^3.0.0", + "intersection-observer": "^0.5.1", "jest-junit": "^3.6.0", "lint-staged": "^7.2.0", "mocha": "^3.1.2", diff --git a/webpack.config.js b/webpack.config.js index 3d815e53b..547dd2612 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -159,6 +159,9 @@ const config = { 'process.env': { VERSION: `"${require('./package.json').version}"`, NODE_ENV: `${JSON.stringify(process.env.NODE_ENV)}`, + TALK_DEFAULT_LAZY_RENDER: `${JSON.stringify( + process.env.TALK_DEFAULT_LAZY_RENDER + )}`, }, }), new webpack.EnvironmentPlugin({ diff --git a/yarn.lock b/yarn.lock index 14380061b..66d21c752 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6042,6 +6042,11 @@ interpret@^1.0.0, interpret@^1.0.1: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" integrity sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ= +intersection-observer@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.5.1.tgz#e340fc56ce74290fe2b2394d1ce88c4353ac6dfa" + integrity sha512-Zd7Plneq82kiXFixs7bX62YnuZ0BMRci9br7io88LwDyF3V43cQMI+G5IiTlTNTt+LsDUppl19J/M2Fp9UkH6g== + invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"