Home Reference Source

utils/ManagedConversationList.jsx

import skygearChat from 'skygear-chat';

/**
 * Convenience class to create Conversation compare functions for sorting.
 * It's just a regular compare function that expects 2 Conversation records.
 *
 * @example
 * new ConversationSorting('title','ascending')
 * => function(a,b) { ... }
 */
export class ConversationSorting {
  /**
   * @param {string} attribute
   * The attribute to sort by, currently supports: 'title', 'last update',
   * 'create time' and 'participant count'.
   * @param {string} order
   * Sorting order, expects either 'ascending' or 'descending'.
   */
  constructor(attribute, order) {
    const attributeMap = {
      'title'             : (c) => c.title || "",
      'last update'       : (c) => c.updatedAt,
      'create time'       : (c) => c.createdAt,
      'participant count' : (c) => c.participant_count,
    };
    const orderMap = {
      'ascending'         : true,
      'descending'        : false,
    };
    if(!(
      attributeMap.hasOwnProperty(attribute) &&
      orderMap.hasOwnProperty(order)
    )) {
      throw new Error('invalid conversation sorting parameters: '+attribute+' '+order);
    }
    return function(a,b) {
      const aValue = attributeMap[attribute](a);
      const bValue = attributeMap[attribute](b);
      const sortAscending = orderMap[order];
      if(aValue > bValue) return sortAscending?  1 : -1;
      if(aValue < bValue) return sortAscending? -1 :  1;
      return 0;
    };
  }
}

/**
 * A managed list of conversations for the current user.
 * Note: You must be logged in to use this.
 */
export default class ManagedConversationList {
  /**
   * @param {object} [options={}]
   * @param {boolean} [options.initialFetch=true]
   * Automatically fetch list of conversations on creation.
   * @param {boolean} [options.pubsubSync=true]
   * Keep list synchronized with server using PubSub.
   * @param {function} [options.sortBy=new ConversationSorting('last update',descending')]
   * How the list is sorted, you can use ConversationSorting wrapper
   * to create one or provide your own.
   */
  constructor({
    initialFetch = true,
    pubsubSync = true,
    sortBy = new ConversationSorting('last update','descending'),
  } = {}) {
    // conversation compare fn (for sorting)
    this._compare = sortBy;
    // conversation IDs in order (without type prefix)
    this._orderedIDs = [];
    // map of conversaton ID => conversation object
    this._conversations = {};
    // map of subscription ID => event handler
    this._updateHandlers = {};

    if(initialFetch) {
      this.fetch();
    }
    if(pubsubSync) {
      skygearChat.subscribe(this._eventHandler.bind(this));
    }
  }
  /**
   * If you used pubsubSync, you must call this function when the list
   * is no longer needed to prevent a memory leak.
   */
  destroy() {
    skygearChat.unsubscribe(this._eventHandler);
  }
  /**
   * @private
   */
  _eventHandler(event) {
    if(event.record_type === 'conversation') {
      console.log(`[conversation event]`, event);
      switch(event.event_type) {
        case 'create':
          this.add(event.record);
          break;
        case 'update':
          this.update(event.record);
          break;
        case 'delete':
          this.remove(event.record._id);
          break;
      }
    }
  }
  /**
   * @private
   */
  _conversationsUpdated() {
    const {_conversations, _compare, _updateHandlers} = this;
    this._orderedIDs = Object.keys(_conversations)
      .map(id => _conversations[id])
      .sort(_compare)
      .map(conversation => conversation._id);
    Object.keys(_updateHandlers)
      .map(key => _updateHandlers[key])
      .forEach(handler => handler(this));
  }
  /**
   * Fetches list of conversations from the server.
   * @return {Promise<ManagedConversationList>}
   * Promise of this object, resolves if the fetch is successful.
   */
  fetch() {
    // FIXME: use the getConversations() API when it is fixed
    return skygearChat
      .getUserConversations()
      .then(results => results.map(uc => {
        uc.$transient.conversation.unread_count = uc.unread_count;
        return uc.$transient.conversation;
      }))
      .then(results => {
        console.log('[fetched conversations]', results);
        results.forEach(conversation => {
          this._conversations[conversation._id] = conversation;
        });
        this._conversationsUpdated();
        return this;
      });
  }
  /**
   * List length (like the array length property)
   * @type {number}
   */
  get length() {
    return this._orderedIDs.length;
  }
  /**
   * Get a Conversation
   * @param {number|string} indexOrID
   * Either the list index (number) or conversation ID (string) without type prefix.
   * @return {Conversation}
   */
  get(indexOrID) {
    const {_conversations, _orderedIDs} = this;
    if (typeof indexOrID === 'number') {
      return _conversations[_orderedIDs[indexOrID]]
    } else {
      return _conversations[indexOrID]
    }
  }
  /**
   * List mapping method (like the array map method)
   * @param {function} mappingFunction
   * @return {Array}
   */
  map(mappingFunction) {
    return this._orderedIDs
      .map(id => this._conversations[id])
      .map(mappingFunction);
  }
  /**
   * List filter method (like the array filter method)
   * @param {function} predicate
   * @return {Array}
   */
  filter(predicate) {
    return this._orderedIDs
      .map(id => this._conversations[id])
      .filter(predicate);
  }
  /**
   * Add a conversation, no-op if the conversation already exists.
   * @param {Conversation} conversation
   * @return {ManagedConversationList}
   */
  add(conversation) {
    const {_conversations} = this;
    if(!_conversations.hasOwnProperty(conversation._id)) {
      _conversations[conversation._id] = conversation;
      this._conversationsUpdated();
    }
    return this;
  }
  /**
   * Update a conversation, no-op if the conversation updatedAt date is older than existing.
   * Conversation will be added if it's not in the list.
   * @param {Conversation} conversation
   * @return {ManagedConversationList}
   */
  update(conversation) {
    const {_conversations} = this;
    if(
      _conversations.hasOwnProperty(conversation._id) &&
      conversation.updatedAt >= _conversations[conversation._id].updatedAt
    ) {
      _conversations[conversation._id] = conversation;
      this._conversationsUpdated();
    } else {
      this.add(conversation);
    }
    return this;
  }
  /**
   * Remove a conversation, no-op if the conversation doesn't exist.
   * @param {string} conversationID Conversation ID without type prefix.
   * @return {ManagedConversationList}
   */
  remove(conversationID) {
    const {_conversations} = this;
    if(_conversations.hasOwnProperty(conversationID)) {
      delete _conversations[conversationID];
      this._conversationsUpdated();
    }
    return this;
  }
  /**
   * Subscribe to list updates, handler will be called with one argument: this object.
   * @param {function} handler
   * @return {number} Subscription ID
   */
  subscribe(handler) {
    const {_updateHandlers} = this;
    const subID = Object.keys(_updateHandlers).length;
    _updateHandlers[subID] = handler;
    return subID;
  }
  /**
   * Cancel a subscription.
   * @param {number} subID The subscription ID returned by the subscribe function.
   */
  unsubscribe(subID) {
    delete this._updateHandlers[subID];
  }
}