client.js

// vim: ts=4:sw=4:expandtab
/* global forsta, ifrpc */


/**
 * @namespace forsta
 */
self.forsta = self.forsta || {};

/**
 * @namespace forsta.messenger
 */
forsta.messenger = forsta.messenger || {};


(function() {
    'use strict';

    const ns = forsta.messenger;

    /**
     * The Forsta messenger client class.
     *
     * @memberof forsta.messenger
     * @fires init
     * @fires loaded
     * @fires thread-message
     * @fires provisioningrequired
     * @fires provisioningerror
     * @fires provisioningdone
     *
     * @example
     * const client = new forsta.messenger.Client(document.querySelector('#myDivId'),
     *                                            {orgEphemeralToken: 'secret'});
     */
    forsta.messenger.Client = class Client {

        /**
         * The client application has been initialized.  This is emitted shortly after successfully
         * starting up, but before the messenger is fully loaded.  Use the `loaded` event to wait
         * for the client application to be completely available.
         *
         * @event init
         * @type {object}
         */

        /**
         * The client application is fully loaded and ready to be controlled.
         *
         * @event loaded
         * @type {object}
         */

        /**
         * This event is emitted if the application requires the user to perform provisioning of
         * their Identity Key.
         *
         * @event provisioningrequired
         * @type {object}
         */

        /**
         * If an error occurs during provisioning it will be emitted using this event.
         *
         * @event provisioningerror
         * @type {object}
         * @property {Error} error - The error object.
         */

        /**
         * When provisioning has finished successfully this event is emitted.
         *
         * @event provisioningdone
         * @type {object}
         */

        /**
         * Thread message event.  Emitted when a new message is added, either by sending
         * or receiving.
         *
         * @event thread-message
         * @type {object}
         * @property {string} id - The message id.
         * @property {string} threadId - The id of the thread this message belongs to.
         */

        /**
         * Auth is a single value union.  Only ONE property should be set.
         *
         *
         * @typedef {Object} ClientAuth
         * @property {string} [orgEphemeralToken] - Org ephemeral user token created at
         *                                          {@link https://app.forsta.io/authtokens}.
         * @property {string} [jwt] - An existing JSON Web Token for a Forsta user account. Note that
         *                            the JWT may be updated during use.  Subscribe to the `jwtupdate`
         *                            event to handle updates made during extended use.
         */

        /**
         * Information about the ephemeral user that will be created or reused for this session.
         *
         *
         * @typedef {Object} EphemeralUserInfo
         * @property {string} [firstName] - First name of the user.
         * @property {string} [lastName] - Last name of the user.
         * @property {string} [email] - Email of the user.
         * @property {string} [phone] - Phone of the user.  NOTE: Should be SMS capable.
         * @property {string} [salt] - Random value used to distinguish user accounts in advanced use-cases.
         */

        /**
         * @typedef {Object} ClientOptions
         * @property {Function} [onInit] - Callback to run when client is first initialized.
         * @property {Function} [onLoaded] - Callback to run when client is fully loaded and ready to use.
         * @property {string} [url=https://app.forsta.io/@] - Override the default site url.
         * @property {bool} showNav - Unhide the navigation panel used for thread selection.
         * @property {bool} showHeader - Unhide the header panel.
         * @property {bool} showThreadAside - Unhide the optional right aside panel containing thread info.
         * @property {bool} showThreadHeader - Unhide the thread header panel.
         * @property {EphemeralUserInfo} ephemeralUserInfo - Details about the ephemeral user to be created or used.
         *                                                   Only relevant when orgEphemeralToken auth is used.
         * @property {null|string} openThreadId - Force the messenger to open a specific thread on
         *                                        startup.  If the value is `null` it will force
         *                                        the messenger to not open any thread.
         */


        /**
         * @param {Element} el - Element where the messenger should be loaded.
         *                       {@link https://developer.mozilla.org/en-US/docs/Web/API/Element}
         * @param {ClientAuth} auth - Auth configuration for Forsta user account.
         * @param {ClientOptions} [options]
         */
        constructor(el, auth, options) {
            if (!(el instanceof Element)) {
                throw new TypeError('el argument must be an Element');
            }
            if (!auth) {
                throw new TypeError('auth argument missing');
            }
            this.auth = auth;
            this.options = options || {};
            this.onInit = this.options.onInit;
            this.onLoaded = this.options.onLoaded;
            this._iframe = document.createElement('iframe');
            this._iframe.style.border = 'none';
            this._iframe.style.width = '100%';
            this._iframe.style.height = '100%';
            const desiredFeatures = new Set([
                'camera',
                'microphone',
                'fullscreen',
                'autoplay',
                'display-capture',
                'geolocation',
                'speaker',
                'vibrate'
            ]);
            if (document.featurePolicy && document.featurePolicy.allowedFeatures) {
                const allowed = new Set(document.featurePolicy.allowedFeatures());
                for (const x of Array.from(desiredFeatures)) {
                    if (!allowed.has(x)) {
                        desiredFeatures.delete(x);
                    }
                }
            }
            this._iframe.setAttribute('allow', Array.from(desiredFeatures).join('; '));
            if (desiredFeatures.has('fullscreen')) {
                // Legacy fullscreen mode required too.
                this._iframe.setAttribute('allowfullscreen', 'true');
            }
            const url = this.options.url || 'https://app.forsta.io/@';
            this._iframe.src = `${url}?managed`;
            el.appendChild(this._iframe);
            this._iframe.contentWindow.addEventListener('beforeunload', ev => {
                console.error("before unload");
            });
            this._iframe.contentWindow.addEventListener('unload', ev => {
                console.error("unload");
            });
            this._iframe.contentWindow.addEventListener('unload', ev => {
                console.error("unload");
            });
            this._iframe.contentWindow.addEventListener('load', ev => {
                console.error("load", this._iframe.src, this._iframe.getAttribute('src'));
            });
            this._iframe.addEventListener('load', ev => {
                console.error("load iframe", ev);
                console.error("load iframe", this._iframe.src, this._iframe.getAttribute('src'));
            });
            this._iframe.addEventListener('loadstart', ev => {
                console.error("loadstart iframe");
            });
            this._iframe.addEventListener('loadend', ev => {
                console.error("loadend iframe");
            });
            this._rpc = ifrpc.init(this._iframe.contentWindow, {acceptOpener: true});
            this._idbGateway = new ns.IDBGateway(this._rpc);
            const _this = this;
            this._rpc.addEventListener('init', function(data) {
                const ev = this;
                _this._onClientInit(ev.source, data);
            });
            if (this.onLoaded) {
                this._rpc.addEventListener('loaded', () => this.onLoaded(this));
            }
        }

        async _onClientInit(frame, data) {
            const config = {
                auth: this.auth
            };
            if (data.scope === 'main') {
                Object.assign(config, {
                    showNav: !!this.options.showNav,
                    showHeader: !!this.options.showHeader,
                    showThreadAside: !!this.options.showThreadAside,
                    showThreadHeader: !!this.options.showThreadHeader,
                    ephemeralUser: this.options.ephemeralUserInfo,
                    openThreadId: this.options.openThreadId,
                });
                if (this._rpcEarlyEvents) {
                    for (const x of this._rpcEarlyEvents) {
                        this._rpc.addEventListener(x.event, x.callback);
                    }
                    delete this._rpcEarlyEvents;
                }
            }
            await this._rpc.invokeCommandWithFrame(frame, 'configure', config);
            if (data.scope === 'main' && this.onInit) {
                await this.onInit(this);
            }
        }

        /**
         * Add an event listener.
         *
         * @param {string} event - Name of the event to listen to.
         * @param {Function} callback - Callback function to invoke.
         */
        addEventListener(event, callback) {
            if (!this._rpc) {
                if (!this._rpcEarlyEvents) {
                    this._rpcEarlyEvents = [];
                }
                this._rpcEarlyEvents.push({event, callback});
            } else {
                this._rpc.addEventListener(event, callback);
            }
        }

        /**
         * Remove an event listener.
         *
         * @param {string} event - Name of the event to stop listening to.
         * @param {Function} callback - Callback function used with {@link addEventListener}.
         */
        removeEventListener(event, callback) {
            if (!this._rpc) {
                this._rpcEarlyEvents = this._rpcEarlyEvents.filter(x =>
                    !(x.event === event && x.callback === callback));
            } else {
                this._rpc.removeEventListener(event, callback);
            }
        }

        /**
         * Expand or collapse the navigation panel.
         *
         * @param {bool} [collapse] - Force the desired collapse state.
         */
        async navPanelToggle(collapse) {
            await this._rpc.invokeCommand('nav-panel-toggle', collapse);
        }

        /**
         * Select or create a conversation thread.  If the tag `expression` argument matches an
         * existing thread it will be opened, otherwise a new thread will be created.
         *
         * @param {string} expression - The {@link TagExpression} for the desired thread's
         *                              distribution.
         * @returns {string} The threadId that was opened.
         */
        async threadStartWithExpression(expression) {
            return await this._rpc.invokeCommand('thread-join', expression);
        }

        /**
         * Open a thread by its `ID`.
         *
         * @param {string} id - The thread ID to open.
         */
        async threadOpen(id) {
            await this._rpc.invokeCommand('thread-open', id);
        }

        /**
         * Set the expiration time for messages in a thread.  When this value is set to a non-zero
         * value, messages will expire from the thread after they are read.  Set this value to `0`
         * to disable the expiration behavior.
         *
         * @param {string} id - The thread ID to update.
         * @param {number} expiration - Expiration time in seconds.  The expiration timer starts
         * when the message is read by the recipient.
         */
        async threadSetExpiration(id, expiration) {
            await this._rpc.invokeCommand('thread-set-expiration', id, expiration);
        }

        /**
         * List threads known to this client.
         *
         * @returns {string[]} - List of thread IDs.
         */
        async threadList() {
            return await this._rpc.invokeCommand('thread-list');
        }

        /**
         * List the attributes of a thread.
         *
         * @param {string} id - The thread ID to update.
         * @returns {string[]} - List of thread attibutes.
         */
        async threadListAttributes(threadId) {
            return await this._rpc.invokeCommand('thread-list-attributes', threadId);
        }

        /**
         * Get the value of a thread attribute.
         *
         * @param {string} id - The thread ID to update.
         * @param {string} attr - The thread attribute to get.
         * @returns {*} - The value of the thread attribute.
         */
        async threadGetAttribute(threadId, attr) {
            return await this._rpc.invokeCommand('thread-get-attribute', threadId, attr);
        }

        /**
         * Set the value of a thread attribute.
         *
         * @param {string} id - The thread ID to update.
         * @param {string} attr - The thread attribute to update.
         * @param {*} value - The value to set.
         */
        async threadSetAttribute(threadId, attr, value) {
            return await this._rpc.invokeCommand('thread-set-attribute', threadId, attr, value);
        }
    };
})();