Home Manual Reference Source

src/core/Invite.js

import {
  ACCEPTED,
  CANCELED,
  ONGOING,
  REJECTED
} from './util/constants';
import * as DataSync from './util/DataSync';
import * as Log from './util/Log';
import cache from './util/cache';
import Room from './Room';
import * as Events from '../definitions/Events';

/**
 * Update
 * @param {Invite} invite The invite
 * @param {string} status The new status
 * @param {string} [reason=null] The reason (a message)
 * @param {object} [_ended=null]
 * @access private
 * @returns {Promise<Invite, Error>}
 */
const update = (invite, status, reason = null, _ended = null) => {
  const values = {
    status,
    reason,
    _ended
  };
  if (invite.status !== ONGOING) {
    return Promise.reject(new Error('This invitation has already been answered'));
  }
  return DataSync.update(`_/invites/${invite.to}/${invite.uid}`, values)
    .then(() => {
      Object.keys(values).forEach((prop) => {
        invite[prop] = values[prop]; // eslint-disable-line no-param-reassign
      });
      return Room.get(invite.room);
    })
    .then(() => ({ invite }))
    .catch(Log.r('Invite_update'));
};

/**
 * Invitation
 * @public
 */
export default class Invite {
  /**
   * Create an invite
   * @param {Webcom/api.DataSnapshot|object} snapData The data snapshot
   * @access protected
   */
  constructor(snapData) {
    let values = snapData;
    if (snapData && snapData.val && typeof snapData.val === 'function') {
      values = Object.assign({},
        snapData.val(),
        { uid: snapData.name(), to: snapData.ref().parent().name() });
    }
    /**
     * Invite's unique id
     * @type string
     */
    this.uid = values.uid;
    /**
     * Invite's sender uid
     * @type {string}
     */
    this.from = values.from;
    /**
     * Invitee's uid
     * @type {string}
     */
    this.to = values.to;
    /**
     * The id of the room associated to the invite
     * @type {string}
     */
    this.room = values.room;
    /**
     * The invitation status :
     * - ONGOING - The receiver has not yet responded to the invitation
     * - ACCEPTED - The receiver has accepted the invitation
     * - REJECTED - The receiver has rejected the invitation
     * - CANCELED - The sender canceled the invitation
     * @type {string}
     */
    this.status = values.status;
    /**
     * Invite message. This will be either a custom message,
     * if the status is ONGOING or a reason when status is CANCELED|REJECTED.
     * @type {string}
     */
    this.topic = values.topic;
    /**
     * Invite creation timestamp
     * @type {number}
     */
    this._created = values._created;
    /**
     * Invite expiration timestamp
     * @type {number}
     */
    this._ended = values._ended;
    /**
     * Invite events callbacks
     * @type {{}}
     * @private
     */
    this._callbacks = {};
  }

  /**
   * Is this invitation waiting for a reply ?
   * @type {boolean}
   */
  get isOnGoing() {
    return this.status === ONGOING;
  }

  /**
   * Was this invitation rejected ?
   * @type {boolean}
   */
  get isRejected() {
    return this.status === REJECTED;
  }

  /**
   * Was this invitation accepted ?
   * @type {boolean}
   */
  get isAccepted() {
    return this.status === ACCEPTED;
  }

  /**
   * Was this invitation canceled ?
   * @type {boolean}
   */
  get isCanceled() {
    return this.status === CANCELED;
  }

  /**
   * Cancels the invitation. Only the sender can cancel the invitation.
   * @param {string} [reason] The reason the sender is canceling the invite
   * @return {Promise<Invite>}
   */
  cancel(reason) {
    return update(this, CANCELED, reason, DataSync.ts());
  }

  /**
   * Rejects the invitation. Only the receiver can reject the invitation.
   * @param {string} [reason] The reason the receiver is rejecting the invite
   * @return {Promise<Invite>}
   */
  reject(reason) {
    return update(this, REJECTED, reason, DataSync.ts());
  }

  /**
   * Accept the invitation. Only the receiver can accept the invitation.
   * @return {Promise<Invite>}
   */
  accept() {
    return update(this, ACCEPTED);
  }

  /**
   * Register a callback for a status update
   * @param {string} status Can be:
   * - ACCEPTED
   * - REJECTED
   * - CANCELED
   * @param {function} callback
   */
  on(status, callback) {
    if (Events.invite.supports(status)) {
      // Register callback
      if (!this._callbacks[status]) {
        this._callbacks[status] = [];
      }
      this._callbacks[status].push(callback);
      // Defined listener & subscribe if needed
      if (!this._listener) {
        /**
         * Invite status update callback
         * @type {function}
         * @private
         */
        this._listener = (snapData) => {
          const updated = snapData.val();
          if (updated !== null && updated !== this.status) {
            this.status = updated;
            (this._callbacks[updated] || []).forEach((cb) => {
              cb(this);
            });
          }
        };
        DataSync.on(`_/invites/${this.to}/${this.uid}/status`, 'value', this._listener.bind(this));
      }
    }
  }

  /**
   * Register a callback for all status change events
   * @param {function} callback
   */
  onStatusChange(callback) {
    [ACCEPTED, REJECTED, CANCELED].forEach((event) => {
      this.on(event, callback);
    });
  }

  /**
   * Un-register a callback for a status update
   * @param {string} [status] Can be:
   * - ACCEPTED
   * - REJECTED
   * - CANCELED
   * - null This will un-register all callbacks
   * @param {function} [callback]
   */
  off(status, callback) {
    if (!status) {
      this._callbacks = {};
    } else if (this._callbacks[status]) {
      if (callback) {
        const idx = this._callbacks[status].findIndex(cb => cb === callback);
        if (idx >= 0) {
          this._callbacks.splice(idx, 1);
        }
      } else {
        this._callbacks[status] = [];
      }
    }
    if (![CANCELED, ACCEPTED, REJECTED]
      .some(event => this._callbacks[event] && this._callbacks[event].length > 0)) {
      DataSync.off(`_/invites/${this.to}/${this.uid}/status`, 'value');
    }
  }

  /**
   * Un-register a callback for all status change events
   * @param {function} [callback]
   */
  offStatusChange(callback) {
    if (!callback) {
      this.off();
    } else {
      [ACCEPTED, REJECTED, CANCELED].forEach((event) => {
        this.off(event, callback);
      });
    }
  }

  /**
   * Create the invitation & add the user to the participants list
   * @access protected
   * @param {User} invitee The user to invite
   * @param {Room} room The room to invite the user to
   * @param {string} [message] A message for the invitee
   */
  static send(invitee, room, message = null) {
    if (!cache.user) {
      return Promise.reject(new Error('Only an authenticated user can send an invite.'));
    }

    const inviteMetaData = {
      from: cache.user.uid,
      room: room.uid,
      status: ONGOING,
      _created: DataSync.ts(),
      topic: message
    };

    return DataSync.push(`_/invites/${invitee.uid}`, inviteMetaData)
      .then((inviteRef) => {
        const inviteId = inviteRef.name();
        return new Invite(Object.assign({ uid: inviteId, to: invitee.uid }, inviteMetaData));
      })
      .catch(Log.r('Invite#send'));
  }
}