forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1030 lines
		
	
	
	
		
			34 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1030 lines
		
	
	
	
		
			34 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | 
						|
 * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
var loop = loop || {};
 | 
						|
loop.contacts = (function(_, mozL10n) {
 | 
						|
  "use strict";
 | 
						|
 | 
						|
  var sharedMixins = loop.shared.mixins;
 | 
						|
 | 
						|
  const Button = loop.shared.views.Button;
 | 
						|
  const ButtonGroup = loop.shared.views.ButtonGroup;
 | 
						|
  const CALL_TYPES = loop.shared.utils.CALL_TYPES;
 | 
						|
 | 
						|
  // Number of contacts to add to the list at the same time.
 | 
						|
  const CONTACTS_CHUNK_SIZE = 100;
 | 
						|
 | 
						|
  // At least this number of contacts should be present for the filter to appear.
 | 
						|
  const MIN_CONTACTS_FOR_FILTERING = 7;
 | 
						|
 | 
						|
  let getContactNames = function(contact) {
 | 
						|
    // The model currently does not enforce a name to be present, but we're
 | 
						|
    // going to assume it is awaiting more advanced validation of required fields
 | 
						|
    // by the model. (See bug 1069918)
 | 
						|
    // NOTE: this method of finding a firstname and lastname is not i18n-proof.
 | 
						|
    let names = contact.name[0].split(" ");
 | 
						|
    return {
 | 
						|
      firstName: names.shift(),
 | 
						|
      lastName: names.join(" ")
 | 
						|
    };
 | 
						|
  };
 | 
						|
 | 
						|
  /** Used to retrieve the preferred email or phone number
 | 
						|
   *  for the contact. Both fields are optional.
 | 
						|
   * @param   {object} contact
 | 
						|
   *          The contact object to get the field from.
 | 
						|
   * @param   {string} field
 | 
						|
   *          The field that should be read out of the contact object.
 | 
						|
   * @returns {object} An object with a 'value' property that hold a string value.
 | 
						|
   */
 | 
						|
  let getPreferred = function(contact, field) {
 | 
						|
    if (!contact[field] || !contact[field].length) {
 | 
						|
      return { value: "" };
 | 
						|
    }
 | 
						|
    return contact[field].find(e => e.pref) || contact[field][0];
 | 
						|
  };
 | 
						|
 | 
						|
  /** Used to set the preferred email or phone number
 | 
						|
   *  for the contact. Both fields are optional.
 | 
						|
   * @param   {object} contact
 | 
						|
   *          The contact object to get the field from.
 | 
						|
   * @param   {string} field
 | 
						|
   *          The field within the contact to set.
 | 
						|
   * @param   {string} value
 | 
						|
   *          The value that the field should be set to.
 | 
						|
   */
 | 
						|
  let setPreferred = function(contact, field, value) {
 | 
						|
    // Don't clear the field if it doesn't exist.
 | 
						|
    if (!value && (!contact[field] || !contact[field].length)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!contact[field]) {
 | 
						|
      contact[field] = [];
 | 
						|
    }
 | 
						|
 | 
						|
    if (!contact[field].length) {
 | 
						|
      contact[field][0] = {"value": value};
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    // Set the value in the preferred tuple and return.
 | 
						|
    for (let i in contact[field]) {
 | 
						|
      if (contact[field][i].pref) {
 | 
						|
        contact[field][i].value = value;
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    contact[field][0].value = value;
 | 
						|
  };
 | 
						|
 | 
						|
  const GravatarPromo = React.createClass({displayName: "GravatarPromo",
 | 
						|
    mixins: [sharedMixins.WindowCloseMixin],
 | 
						|
 | 
						|
    propTypes: {
 | 
						|
      handleUse: React.PropTypes.func.isRequired
 | 
						|
    },
 | 
						|
 | 
						|
    getInitialState: function() {
 | 
						|
      return {
 | 
						|
        showMe: navigator.mozLoop.getLoopPref("contacts.gravatars.promo") &&
 | 
						|
          !navigator.mozLoop.getLoopPref("contacts.gravatars.show")
 | 
						|
      };
 | 
						|
    },
 | 
						|
 | 
						|
    handleCloseButtonClick: function() {
 | 
						|
      navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
 | 
						|
      this.setState({ showMe: false });
 | 
						|
    },
 | 
						|
 | 
						|
    handleLinkClick: function(event) {
 | 
						|
      if (!event.target || !event.target.href) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      event.preventDefault();
 | 
						|
      navigator.mozLoop.openURL(event.target.href);
 | 
						|
      this.closeWindow();
 | 
						|
    },
 | 
						|
 | 
						|
    handleUseButtonClick: function() {
 | 
						|
      navigator.mozLoop.setLoopPref("contacts.gravatars.promo", false);
 | 
						|
      navigator.mozLoop.setLoopPref("contacts.gravatars.show", true);
 | 
						|
      this.setState({ showMe: false });
 | 
						|
      this.props.handleUse();
 | 
						|
    },
 | 
						|
 | 
						|
    render: function() {
 | 
						|
      if (!this.state.showMe) {
 | 
						|
        return null;
 | 
						|
      }
 | 
						|
 | 
						|
      let privacyUrl = navigator.mozLoop.getLoopPref("legal.privacy_url");
 | 
						|
      let message = mozL10n.get("gravatars_promo_message", {
 | 
						|
        "learn_more": React.renderToStaticMarkup(
 | 
						|
          React.createElement("a", {href: privacyUrl, target: "_blank"}, 
 | 
						|
            mozL10n.get("gravatars_promo_message_learnmore")
 | 
						|
          )
 | 
						|
        )
 | 
						|
      });
 | 
						|
      return (
 | 
						|
        React.createElement("div", {className: "contacts-gravatar-promo"}, 
 | 
						|
          React.createElement(Button, {additionalClass: "button-close", 
 | 
						|
                  caption: "", 
 | 
						|
                  onClick: this.handleCloseButtonClick}), 
 | 
						|
          React.createElement("p", {dangerouslySetInnerHTML: {__html: message}, 
 | 
						|
             onClick: this.handleLinkClick}), 
 | 
						|
          React.createElement("div", {className: "contacts-gravatar-avatars"}, 
 | 
						|
            React.createElement("img", {src: "loop/shared/img/avatars.svg#orange-avatar"}), 
 | 
						|
            React.createElement("span", {className: "contacts-gravatar-arrow"}), 
 | 
						|
            React.createElement("img", {src: "loop/shared/img/firefox-avatar.svg"})
 | 
						|
          ), 
 | 
						|
          React.createElement(ButtonGroup, {additionalClass: "contacts-gravatar-buttons"}, 
 | 
						|
            React.createElement(Button, {additionalClass: "secondary", 
 | 
						|
                    caption: mozL10n.get("gravatars_promo_button_nothanks2"), 
 | 
						|
                    onClick: this.handleCloseButtonClick}), 
 | 
						|
            React.createElement(Button, {additionalClass: "secondary", 
 | 
						|
                    caption: mozL10n.get("gravatars_promo_button_use2"), 
 | 
						|
                    onClick: this.handleUseButtonClick})
 | 
						|
          )
 | 
						|
        )
 | 
						|
      );
 | 
						|
    }
 | 
						|
  });
 | 
						|
 | 
						|
  const ContactDropdown = React.createClass({displayName: "ContactDropdown",
 | 
						|
    propTypes: {
 | 
						|
      // If the contact is blocked or not.
 | 
						|
      blocked: React.PropTypes.bool,
 | 
						|
      canEdit: React.PropTypes.bool,
 | 
						|
      // Position of mouse when opening menu
 | 
						|
      eventPosY: React.PropTypes.number.isRequired,
 | 
						|
      // callback function that provides height and top coordinate for contacts container
 | 
						|
      getContainerCoordinates: React.PropTypes.func.isRequired,
 | 
						|
      handleAction: React.PropTypes.func.isRequired
 | 
						|
    },
 | 
						|
 | 
						|
    getInitialState: function() {
 | 
						|
      return {
 | 
						|
        openDirUp: false
 | 
						|
      };
 | 
						|
    },
 | 
						|
 | 
						|
    onItemClick: function(event) {
 | 
						|
      this.props.handleAction(event.currentTarget.dataset.action);
 | 
						|
    },
 | 
						|
 | 
						|
    componentDidMount: function() {
 | 
						|
      var menuNode = this.getDOMNode();
 | 
						|
      var menuNodeRect = menuNode.getBoundingClientRect();
 | 
						|
      var listNodeCoords = this.props.getContainerCoordinates();
 | 
						|
 | 
						|
      // Click offset to not display the menu right next to the area clicked.
 | 
						|
      var offset = 10;
 | 
						|
 | 
						|
      if (this.props.eventPosY + menuNodeRect.height >=
 | 
						|
        listNodeCoords.top + listNodeCoords.height) {
 | 
						|
 | 
						|
        // Position above click area.
 | 
						|
        menuNode.style.top = this.props.eventPosY - menuNodeRect.height
 | 
						|
          - offset + "px";
 | 
						|
      } else {
 | 
						|
        // Position below click area.
 | 
						|
        menuNode.style.top = this.props.eventPosY + offset + "px";
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    render: function() {
 | 
						|
      var cx = React.addons.classSet;
 | 
						|
      var dropdownClasses = cx({
 | 
						|
        "dropdown-menu": true,
 | 
						|
        "dropdown-menu-up": this.state.openDirUp
 | 
						|
      });
 | 
						|
      let blockAction = this.props.blocked ? "unblock" : "block";
 | 
						|
      let blockLabel = this.props.blocked ? "unblock_contact_menu_button"
 | 
						|
                                          : "block_contact_menu_button";
 | 
						|
 | 
						|
      return (
 | 
						|
        React.createElement("ul", {className: dropdownClasses}, 
 | 
						|
          React.createElement("li", {className: cx({ "dropdown-menu-item": true,
 | 
						|
                              "disabled": this.props.blocked,
 | 
						|
                              "video-call-item": true }), 
 | 
						|
              "data-action": "video-call", 
 | 
						|
              onClick: this.onItemClick}, 
 | 
						|
            mozL10n.get("video_call_menu_button")
 | 
						|
          ), 
 | 
						|
          React.createElement("li", {className: cx({ "dropdown-menu-item": true,
 | 
						|
                              "disabled": this.props.blocked,
 | 
						|
                              "audio-call-item": true }), 
 | 
						|
              "data-action": "audio-call", 
 | 
						|
              onClick: this.onItemClick}, 
 | 
						|
            mozL10n.get("audio_call_menu_button")
 | 
						|
          ), 
 | 
						|
          React.createElement("li", {className: cx({ "dropdown-menu-item": true,
 | 
						|
                              "disabled": !this.props.canEdit }), 
 | 
						|
              "data-action": "edit", 
 | 
						|
              onClick: this.onItemClick}, 
 | 
						|
            mozL10n.get("edit_contact_title")
 | 
						|
          ), 
 | 
						|
          React.createElement("li", {className: "dropdown-menu-item", 
 | 
						|
              "data-action": blockAction, 
 | 
						|
              onClick: this.onItemClick}, 
 | 
						|
            mozL10n.get(blockLabel)
 | 
						|
          ), 
 | 
						|
          React.createElement("li", {className: cx({ "dropdown-menu-item": true,
 | 
						|
                              "disabled": !this.props.canEdit }), 
 | 
						|
              "data-action": "remove", 
 | 
						|
              onClick: this.onItemClick}, 
 | 
						|
            mozL10n.get("confirm_delete_contact_remove_button")
 | 
						|
          )
 | 
						|
        )
 | 
						|
      );
 | 
						|
    }
 | 
						|
  });
 | 
						|
 | 
						|
  const ContactDetail = React.createClass({displayName: "ContactDetail",
 | 
						|
    propTypes: {
 | 
						|
      contact: React.PropTypes.object.isRequired,
 | 
						|
      getContainerCoordinates: React.PropTypes.func.isRequired,
 | 
						|
      handleContactAction: React.PropTypes.func
 | 
						|
    },
 | 
						|
 | 
						|
    mixins: [
 | 
						|
      sharedMixins.DropdownMenuMixin()
 | 
						|
    ],
 | 
						|
 | 
						|
    getInitialState: function() {
 | 
						|
      return {
 | 
						|
        eventPosY: 0
 | 
						|
      };
 | 
						|
    },
 | 
						|
 | 
						|
    handleShowDropdownClick: function(e) {
 | 
						|
      e.preventDefault();
 | 
						|
      e.stopPropagation();
 | 
						|
 | 
						|
      this.setState({
 | 
						|
        eventPosY: e.pageY
 | 
						|
      });
 | 
						|
 | 
						|
      this.toggleDropdownMenu();
 | 
						|
    },
 | 
						|
 | 
						|
    hideDropdownMenuHandler: function() {
 | 
						|
      // Since this call may be deferred, we need to guard it, for example in
 | 
						|
      // case the contact was removed in the meantime.
 | 
						|
      if (this.isMounted()) {
 | 
						|
        this.hideDropdownMenu();
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    shouldComponentUpdate: function(nextProps, nextState) {
 | 
						|
      let currContact = this.props.contact;
 | 
						|
      let nextContact = nextProps.contact;
 | 
						|
      let currContactEmail = getPreferred(currContact, "email").value;
 | 
						|
      let nextContactEmail = getPreferred(nextContact, "email").value;
 | 
						|
      return (
 | 
						|
        currContact.name[0] !== nextContact.name[0] ||
 | 
						|
        currContact.blocked !== nextContact.blocked ||
 | 
						|
        currContactEmail !== nextContactEmail ||
 | 
						|
        nextState.showMenu !== this.state.showMenu
 | 
						|
      );
 | 
						|
    },
 | 
						|
 | 
						|
    handleAction: function(actionName) {
 | 
						|
      if (this.props.handleContactAction) {
 | 
						|
        this.props.handleContactAction(this.props.contact, actionName);
 | 
						|
        this.hideDropdownMenuHandler();
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    canEdit: function() {
 | 
						|
      // We cannot modify imported contacts.  For the moment, the check for
 | 
						|
      // determining whether the contact is imported is based on its category.
 | 
						|
      return this.props.contact.category[0] !== "google";
 | 
						|
    },
 | 
						|
 | 
						|
    /**
 | 
						|
     * Callback called when moving cursor away from the conversation entry.
 | 
						|
     * Will close the dropdown menu.
 | 
						|
     */
 | 
						|
    _handleMouseOut: function() {
 | 
						|
      if (this.state.showMenu) {
 | 
						|
        this.toggleDropdownMenu();
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    render: function() {
 | 
						|
      let names = getContactNames(this.props.contact);
 | 
						|
      let email = getPreferred(this.props.contact, "email");
 | 
						|
      let avatarSrc = navigator.mozLoop.getUserAvatar(email.value);
 | 
						|
      let cx = React.addons.classSet;
 | 
						|
      let contactCSSClass = cx({
 | 
						|
        contact: true,
 | 
						|
        blocked: this.props.contact.blocked
 | 
						|
      });
 | 
						|
      let avatarCSSClass = cx({
 | 
						|
        avatar: true,
 | 
						|
        defaultAvatar: !avatarSrc
 | 
						|
      });
 | 
						|
      return (
 | 
						|
        React.createElement("li", {className: contactCSSClass, 
 | 
						|
            onMouseLeave: this._handleMouseOut}, 
 | 
						|
          React.createElement("div", {className: avatarCSSClass}, 
 | 
						|
            avatarSrc ? React.createElement("img", {src: avatarSrc}) : null
 | 
						|
          ), 
 | 
						|
          React.createElement("div", {className: "details"}, 
 | 
						|
            React.createElement("div", {className: "username"}, React.createElement("strong", null, names.firstName), " ", names.lastName, 
 | 
						|
              React.createElement("i", {className: cx({"icon icon-blocked": this.props.contact.blocked})})
 | 
						|
            ), 
 | 
						|
            React.createElement("div", {className: "email"}, email.value)
 | 
						|
          ), 
 | 
						|
          React.createElement("div", {className: "icons"}, 
 | 
						|
            React.createElement("i", {className: "icon icon-contact-video-call", 
 | 
						|
               onClick: this.handleAction.bind(null, "video-call")}), 
 | 
						|
            React.createElement("i", {className: "icon icon-vertical-ellipsis icon-contact-menu-button", 
 | 
						|
               onClick: this.handleShowDropdownClick})
 | 
						|
          ), 
 | 
						|
          this.state.showMenu
 | 
						|
            ? React.createElement(ContactDropdown, {blocked: this.props.contact.blocked, 
 | 
						|
                               canEdit: this.canEdit(), 
 | 
						|
                               eventPosY: this.state.eventPosY, 
 | 
						|
                               getContainerCoordinates: this.props.getContainerCoordinates, 
 | 
						|
                               handleAction: this.handleAction})
 | 
						|
            : null
 | 
						|
          
 | 
						|
        )
 | 
						|
      );
 | 
						|
    }
 | 
						|
  });
 | 
						|
 | 
						|
  const ContactsList = React.createClass({displayName: "ContactsList",
 | 
						|
    mixins: [
 | 
						|
      React.addons.LinkedStateMixin,
 | 
						|
      loop.shared.mixins.WindowCloseMixin
 | 
						|
    ],
 | 
						|
 | 
						|
    propTypes: {
 | 
						|
      mozLoop: React.PropTypes.object.isRequired,
 | 
						|
      notifications: React.PropTypes.instanceOf(loop.shared.models.NotificationCollection).isRequired,
 | 
						|
      switchToContactAdd: React.PropTypes.func.isRequired,
 | 
						|
      switchToContactEdit: React.PropTypes.func.isRequired
 | 
						|
    },
 | 
						|
 | 
						|
    /**
 | 
						|
     * Contacts collection object
 | 
						|
     */
 | 
						|
    contacts: null,
 | 
						|
 | 
						|
    /**
 | 
						|
     * User profile
 | 
						|
     */
 | 
						|
    _userProfile: null,
 | 
						|
 | 
						|
    getInitialState: function() {
 | 
						|
      return {
 | 
						|
        importBusy: false,
 | 
						|
        filter: ""
 | 
						|
      };
 | 
						|
    },
 | 
						|
 | 
						|
    refresh: function(callback = function() {}) {
 | 
						|
      let contactsAPI = this.props.mozLoop.contacts;
 | 
						|
 | 
						|
      this.handleContactRemoveAll();
 | 
						|
 | 
						|
      contactsAPI.getAll((err, contacts) => {
 | 
						|
        if (err) {
 | 
						|
          callback(err);
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        // Add contacts already present in the DB. We do this in timed chunks to
 | 
						|
        // circumvent blocking the main event loop.
 | 
						|
        let addContactsInChunks = () => {
 | 
						|
          contacts.splice(0, CONTACTS_CHUNK_SIZE).forEach(contact => {
 | 
						|
            this.handleContactAddOrUpdate(contact, false);
 | 
						|
          });
 | 
						|
          if (contacts.length) {
 | 
						|
            setTimeout(addContactsInChunks, 0);
 | 
						|
          } else {
 | 
						|
            callback();
 | 
						|
          }
 | 
						|
          this.forceUpdate();
 | 
						|
        };
 | 
						|
 | 
						|
        addContactsInChunks(contacts);
 | 
						|
      });
 | 
						|
    },
 | 
						|
 | 
						|
    componentWillMount: function() {
 | 
						|
      // Take the time to initialize class variables that are used outside
 | 
						|
      // `this.state`.
 | 
						|
      this.contacts = {};
 | 
						|
      this._userProfile = this.props.mozLoop.userProfile;
 | 
						|
    },
 | 
						|
 | 
						|
    componentDidMount: function() {
 | 
						|
      window.addEventListener("LoopStatusChanged", this._onStatusChanged);
 | 
						|
 | 
						|
      this.refresh(err => {
 | 
						|
        if (err) {
 | 
						|
          throw err;
 | 
						|
        }
 | 
						|
 | 
						|
        let contactsAPI = this.props.mozLoop.contacts;
 | 
						|
 | 
						|
        // Listen for contact changes/ updates.
 | 
						|
        contactsAPI.on("add", (eventName, contact) => {
 | 
						|
          this.handleContactAddOrUpdate(contact);
 | 
						|
        });
 | 
						|
        contactsAPI.on("remove", (eventName, contact) => {
 | 
						|
          this.handleContactRemove(contact);
 | 
						|
        });
 | 
						|
        contactsAPI.on("removeAll", () => {
 | 
						|
          this.handleContactRemoveAll();
 | 
						|
        });
 | 
						|
        contactsAPI.on("update", (eventName, contact) => {
 | 
						|
          this.handleContactAddOrUpdate(contact);
 | 
						|
        });
 | 
						|
      });
 | 
						|
    },
 | 
						|
 | 
						|
    componentWillUnmount: function() {
 | 
						|
      window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
 | 
						|
    },
 | 
						|
 | 
						|
    /*
 | 
						|
     * Filter a user by name, email or phone number.
 | 
						|
     * Takes in an input to filter by and returns a filter function which
 | 
						|
     * expects a contact.
 | 
						|
     *
 | 
						|
     * @returns {Function}
 | 
						|
     */
 | 
						|
    filterContact: function(filter) {
 | 
						|
      return function(contact) {
 | 
						|
        return getPreferred(contact, "name").toLocaleLowerCase().includes(filter) ||
 | 
						|
          getPreferred(contact, "email").value.toLocaleLowerCase().includes(filter) ||
 | 
						|
          getPreferred(contact, "tel").value.toLocaleLowerCase().includes(filter);
 | 
						|
      };
 | 
						|
    },
 | 
						|
 | 
						|
    /*
 | 
						|
     * Takes all contacts, it groups and filters them before rendering.
 | 
						|
     */
 | 
						|
    _filterContactsList: function() {
 | 
						|
      let shownContacts = _.groupBy(this.contacts, function(contact) {
 | 
						|
        return contact.blocked ? "blocked" : "available";
 | 
						|
      });
 | 
						|
 | 
						|
      if (this._shouldShowFilter()) {
 | 
						|
        let filter = this.state.filter.trim().toLocaleLowerCase();
 | 
						|
        let filterFn = this.filterContact(filter);
 | 
						|
        if (filter) {
 | 
						|
          if (shownContacts.available) {
 | 
						|
            shownContacts.available = shownContacts.available.filter(filterFn);
 | 
						|
            // Filter can return an empty array.
 | 
						|
            if (!shownContacts.available.length) {
 | 
						|
              shownContacts.available = null;
 | 
						|
            }
 | 
						|
          }
 | 
						|
          if (shownContacts.blocked) {
 | 
						|
            shownContacts.blocked = shownContacts.blocked.filter(filterFn);
 | 
						|
            // Filter can return an empty array.
 | 
						|
            if (!shownContacts.blocked.length) {
 | 
						|
              shownContacts.blocked = null;
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      return shownContacts;
 | 
						|
    },
 | 
						|
 | 
						|
    /*
 | 
						|
     * Decide to render contacts filter based on the number of contacts.
 | 
						|
     *
 | 
						|
     * @returns {bool}
 | 
						|
     */
 | 
						|
    _shouldShowFilter: function() {
 | 
						|
      return Object.getOwnPropertyNames(this.contacts).length >=
 | 
						|
        MIN_CONTACTS_FOR_FILTERING;
 | 
						|
    },
 | 
						|
 | 
						|
    _onStatusChanged: function() {
 | 
						|
      let profile = this.props.mozLoop.userProfile;
 | 
						|
      let currUid = this._userProfile ? this._userProfile.uid : null;
 | 
						|
      let newUid = profile ? profile.uid : null;
 | 
						|
      if (currUid !== newUid) {
 | 
						|
        // On profile change (login, logout), reload all contacts.
 | 
						|
        this._userProfile = profile;
 | 
						|
        // The following will do a forceUpdate() for us.
 | 
						|
        this.refresh();
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    handleContactAddOrUpdate: function(contact, render = true) {
 | 
						|
      let contacts = this.contacts;
 | 
						|
      let guid = String(contact._guid);
 | 
						|
      contacts[guid] = contact;
 | 
						|
      if (render) {
 | 
						|
        this.forceUpdate();
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    handleContactRemove: function(contact) {
 | 
						|
      let contacts = this.contacts;
 | 
						|
      let guid = String(contact._guid);
 | 
						|
      if (!contacts[guid]) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      delete contacts[guid];
 | 
						|
      this.forceUpdate();
 | 
						|
    },
 | 
						|
 | 
						|
    handleContactRemoveAll: function() {
 | 
						|
      // Do not allow any race conditions when removing all contacts.
 | 
						|
      this.contacts = {};
 | 
						|
      this.forceUpdate();
 | 
						|
    },
 | 
						|
 | 
						|
    handleImportButtonClick: function() {
 | 
						|
      this.setState({ importBusy: true });
 | 
						|
      this.props.mozLoop.startImport({
 | 
						|
        service: "google"
 | 
						|
      }, (err, stats) => {
 | 
						|
        this.setState({ importBusy: false });
 | 
						|
        if (err) {
 | 
						|
          console.error("Contact import error", err);
 | 
						|
          this.props.notifications.errorL10n("import_contacts_failure_message");
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        this.props.notifications.successL10n("import_contacts_success_message", {
 | 
						|
          num: stats.success,
 | 
						|
          total: stats.success
 | 
						|
        });
 | 
						|
      });
 | 
						|
    },
 | 
						|
 | 
						|
    handleAddContactButtonClick: function() {
 | 
						|
      this.props.switchToContactAdd();
 | 
						|
    },
 | 
						|
 | 
						|
    handleContactAction: function(contact, actionName) {
 | 
						|
      switch (actionName) {
 | 
						|
        case "edit":
 | 
						|
          this.props.switchToContactEdit(contact);
 | 
						|
          break;
 | 
						|
        case "remove":
 | 
						|
          this.props.mozLoop.confirm({
 | 
						|
            message: mozL10n.get("confirm_delete_contact_alert"),
 | 
						|
            okButton: mozL10n.get("confirm_delete_contact_remove_button"),
 | 
						|
            cancelButton: mozL10n.get("confirm_delete_contact_cancel_button")
 | 
						|
          }, (error, result) => {
 | 
						|
            if (error) {
 | 
						|
              throw error;
 | 
						|
            }
 | 
						|
 | 
						|
            if (!result) {
 | 
						|
              return;
 | 
						|
            }
 | 
						|
 | 
						|
            this.props.mozLoop.contacts.remove(contact._guid, err => {
 | 
						|
              if (err) {
 | 
						|
                throw err;
 | 
						|
              }
 | 
						|
            });
 | 
						|
          });
 | 
						|
          break;
 | 
						|
        case "block":
 | 
						|
        case "unblock":
 | 
						|
          // Invoke the API named like the action.
 | 
						|
          this.props.mozLoop.contacts[actionName](contact._guid, err => {
 | 
						|
            if (err) {
 | 
						|
              throw err;
 | 
						|
            }
 | 
						|
          });
 | 
						|
          break;
 | 
						|
        case "video-call":
 | 
						|
          if (!contact.blocked) {
 | 
						|
            this.props.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
 | 
						|
            this.closeWindow();
 | 
						|
          }
 | 
						|
          break;
 | 
						|
        case "audio-call":
 | 
						|
          if (!contact.blocked) {
 | 
						|
            this.props.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
 | 
						|
            this.closeWindow();
 | 
						|
          }
 | 
						|
          break;
 | 
						|
        default:
 | 
						|
          console.error("Unrecognized action: " + actionName);
 | 
						|
          break;
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    handleUseGravatar: function() {
 | 
						|
      // We got permission to use Gravatar icons now, so we need to redraw the
 | 
						|
      // list entirely to show them.
 | 
						|
      this.refresh();
 | 
						|
    },
 | 
						|
 | 
						|
    /*
 | 
						|
     * Callback triggered when clicking the `X` from the contacts filter.
 | 
						|
     * Clears the search query.
 | 
						|
     */
 | 
						|
    _handleFilterClear: function() {
 | 
						|
      this.setState({
 | 
						|
        filter: ""
 | 
						|
      });
 | 
						|
    },
 | 
						|
 | 
						|
    sortContacts: function(contact1, contact2) {
 | 
						|
      let comp = contact1.name[0].localeCompare(contact2.name[0]);
 | 
						|
      if (comp !== 0) {
 | 
						|
        return comp;
 | 
						|
      }
 | 
						|
      // If names are equal, compare against unique ids to make sure we have
 | 
						|
      // consistent ordering.
 | 
						|
      return contact1._guid - contact2._guid;
 | 
						|
    },
 | 
						|
 | 
						|
    getCoordinates: function() {
 | 
						|
      // Returns coordinates for use by child elements to place menus etc that are absolutely positioned
 | 
						|
      var domNode = this.getDOMNode();
 | 
						|
      var domNodeRect = domNode.getBoundingClientRect();
 | 
						|
 | 
						|
      return {
 | 
						|
        "top": domNodeRect.top,
 | 
						|
        "height": domNodeRect.height
 | 
						|
      };
 | 
						|
    },
 | 
						|
 | 
						|
    _renderFilterClearButton: function() {
 | 
						|
      if (this.state.filter) {
 | 
						|
        return (
 | 
						|
          React.createElement("button", {className: "clear-search", 
 | 
						|
                  onClick: this._handleFilterClear})
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      return null;
 | 
						|
    },
 | 
						|
 | 
						|
    _renderContactsFilter: function() {
 | 
						|
      if (this._shouldShowFilter()) {
 | 
						|
        return (
 | 
						|
          React.createElement("div", {className: "contact-filter-container"}, 
 | 
						|
            React.createElement("input", {className: "contact-filter", 
 | 
						|
                   placeholder: mozL10n.get("contacts_search_placesholder2"), 
 | 
						|
                   valueLink: this.linkState("filter")}), 
 | 
						|
            this._renderFilterClearButton()
 | 
						|
          )
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      return null;
 | 
						|
    },
 | 
						|
 | 
						|
    _renderContactsList: function() {
 | 
						|
      let cx = React.addons.classSet;
 | 
						|
      let shownContacts = this._filterContactsList();
 | 
						|
      let viewForItem = item => {
 | 
						|
        return (
 | 
						|
          React.createElement(ContactDetail, {contact: item, 
 | 
						|
                         getContainerCoordinates: this.getCoordinates, 
 | 
						|
                         handleContactAction: this.handleContactAction, 
 | 
						|
                         key: item._guid})
 | 
						|
        );
 | 
						|
      };
 | 
						|
 | 
						|
      // If no contacts to show and filter is set, then none match the search.
 | 
						|
      if (!shownContacts.available && !shownContacts.blocked &&
 | 
						|
          this.state.filter) {
 | 
						|
        return (
 | 
						|
          React.createElement("div", {className: "contact-search-list-empty"}, 
 | 
						|
            React.createElement("p", {className: "panel-text-medium"}, 
 | 
						|
              mozL10n.get("contacts_no_search_results")
 | 
						|
            )
 | 
						|
          )
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      // If no contacts to show and filter is not set, we don't have contacts.
 | 
						|
      if (!shownContacts.available && !shownContacts.blocked &&
 | 
						|
          !this.state.filter) {
 | 
						|
        return (
 | 
						|
            React.createElement("div", {className: "contact-list-empty-container"}, 
 | 
						|
              this._renderGravatarPromoMessage(), 
 | 
						|
              React.createElement("div", {className: "contact-list-empty"}, 
 | 
						|
                React.createElement("p", {className: "panel-text-large"}, 
 | 
						|
                  mozL10n.get("no_contacts_message_heading2")
 | 
						|
                ), 
 | 
						|
                React.createElement("p", {className: "panel-text-medium"}, 
 | 
						|
                  mozL10n.get("no_contacts_import_or_add2")
 | 
						|
                )
 | 
						|
              )
 | 
						|
            )
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      return (
 | 
						|
          React.createElement("div", {className: "contact-list-container"}, 
 | 
						|
            !this.state.filter ? React.createElement("div", {className: "contact-list-title"}, 
 | 
						|
              mozL10n.get("contact_list_title")
 | 
						|
            ) : null, 
 | 
						|
            React.createElement("div", {className: "contact-list-wrapper"}, 
 | 
						|
              this._renderGravatarPromoMessage(), 
 | 
						|
              React.createElement("ul", {className: "contact-list"}, 
 | 
						|
                shownContacts.available ?
 | 
						|
                    shownContacts.available.sort(this.sortContacts).map(viewForItem) :
 | 
						|
                    null, 
 | 
						|
                shownContacts.blocked && shownContacts.blocked.length > 0 ?
 | 
						|
                    React.createElement("div", {className: "contact-separator"}, mozL10n.get("contacts_blocked_contacts")) :
 | 
						|
                    null, 
 | 
						|
                shownContacts.blocked ?
 | 
						|
                    shownContacts.blocked.sort(this.sortContacts).map(viewForItem) :
 | 
						|
                    null
 | 
						|
              )
 | 
						|
            )
 | 
						|
          )
 | 
						|
      );
 | 
						|
    },
 | 
						|
 | 
						|
    _renderAddContactButtons: function() {
 | 
						|
      let cx = React.addons.classSet;
 | 
						|
 | 
						|
      if (this.state.filter) {
 | 
						|
        return null;
 | 
						|
      }
 | 
						|
 | 
						|
      return (
 | 
						|
        React.createElement(ButtonGroup, {additionalClass: "contact-controls"}, 
 | 
						|
          React.createElement(Button, {additionalClass: "secondary", 
 | 
						|
            caption: this.state.importBusy ? mozL10n.get("importing_contacts_progress_button") :
 | 
						|
                                             mozL10n.get("import_contacts_button3"), 
 | 
						|
              disabled: this.state.importBusy, 
 | 
						|
              onClick: this.handleImportButtonClick}, 
 | 
						|
              React.createElement("div", {className: cx({"contact-import-spinner": true,
 | 
						|
                                 spinner: true,
 | 
						|
              busy: this.state.importBusy})})
 | 
						|
          ), 
 | 
						|
          React.createElement(Button, {additionalClass: "primary", 
 | 
						|
            caption: mozL10n.get("new_contact_button2"), 
 | 
						|
            onClick: this.handleAddContactButtonClick})
 | 
						|
        )
 | 
						|
      );
 | 
						|
    },
 | 
						|
 | 
						|
    _renderGravatarPromoMessage: function() {
 | 
						|
      if (this.state.filter) {
 | 
						|
        return null;
 | 
						|
      }
 | 
						|
 | 
						|
      return (
 | 
						|
        React.createElement(GravatarPromo, {handleUse: this.handleUseGravatar})
 | 
						|
      );
 | 
						|
    },
 | 
						|
 | 
						|
    render: function() {
 | 
						|
      return (
 | 
						|
        React.createElement("div", {className: "contacts-container"}, 
 | 
						|
          this._renderContactsFilter(), 
 | 
						|
          this._renderContactsList(), 
 | 
						|
          this._renderAddContactButtons()
 | 
						|
        )
 | 
						|
      );
 | 
						|
    }
 | 
						|
  });
 | 
						|
 | 
						|
  const ContactsControllerView = React.createClass({displayName: "ContactsControllerView",
 | 
						|
    propTypes: {
 | 
						|
      initialSelectedTabComponent: React.PropTypes.string,
 | 
						|
      mozLoop: React.PropTypes.object.isRequired,
 | 
						|
      notifications: React.PropTypes.object.isRequired
 | 
						|
    },
 | 
						|
 | 
						|
    getInitialState: function() {
 | 
						|
      return {
 | 
						|
        currentComponent: this.props.initialSelectedTabComponent || "contactList",
 | 
						|
        contactFormData: {}
 | 
						|
      };
 | 
						|
    },
 | 
						|
 | 
						|
    /* XXX We should have success/Fail callbacks that the children call instead of this
 | 
						|
    * Children should not have knowledge of other views
 | 
						|
    * However, this is being implemented in this way so the view can be directed appropriately
 | 
						|
    * without making it too complex
 | 
						|
    */
 | 
						|
    switchComponentView: function(componentName) {
 | 
						|
      return function() {
 | 
						|
        this.setState({currentComponent: componentName});
 | 
						|
      }.bind(this);
 | 
						|
    },
 | 
						|
 | 
						|
    handleAddEditContact: function(componentName) {
 | 
						|
      return function(contactFormData) {
 | 
						|
        this.setState({
 | 
						|
          contactFormData: contactFormData || {},
 | 
						|
          currentComponent: componentName
 | 
						|
        });
 | 
						|
      }.bind(this);
 | 
						|
    },
 | 
						|
 | 
						|
    /* XXX Consider whether linkedStated makes sense for this */
 | 
						|
    render: function() {
 | 
						|
      switch(this.state.currentComponent) {
 | 
						|
        case "contactAdd":
 | 
						|
          return (
 | 
						|
            React.createElement(ContactDetailsForm, {
 | 
						|
              contactFormData: this.state.contactFormData, 
 | 
						|
              mode: "add", 
 | 
						|
              mozLoop: this.props.mozLoop, 
 | 
						|
              ref: "contacts_add", 
 | 
						|
              switchToInitialView: this.switchComponentView("contactList")})
 | 
						|
          );
 | 
						|
        case "contactEdit":
 | 
						|
          return (
 | 
						|
            React.createElement(ContactDetailsForm, {
 | 
						|
              contactFormData: this.state.contactFormData, 
 | 
						|
              mode: "edit", 
 | 
						|
              mozLoop: this.props.mozLoop, 
 | 
						|
              ref: "contacts_edit", 
 | 
						|
              switchToInitialView: this.switchComponentView("contactList")})
 | 
						|
          );
 | 
						|
        case "contactList":
 | 
						|
        default:
 | 
						|
          return (
 | 
						|
            React.createElement(ContactsList, {
 | 
						|
              mozLoop: this.props.mozLoop, 
 | 
						|
              notifications: this.props.notifications, 
 | 
						|
              ref: "contacts_list", 
 | 
						|
              switchToContactAdd: this.handleAddEditContact("contactAdd"), 
 | 
						|
              switchToContactEdit: this.handleAddEditContact("contactEdit")})
 | 
						|
          );
 | 
						|
      }
 | 
						|
    }
 | 
						|
  });
 | 
						|
 | 
						|
  const ContactDetailsForm = React.createClass({displayName: "ContactDetailsForm",
 | 
						|
    mixins: [React.addons.LinkedStateMixin],
 | 
						|
 | 
						|
    propTypes: {
 | 
						|
      contactFormData: React.PropTypes.object.isRequired,
 | 
						|
      mode: React.PropTypes.string,
 | 
						|
      mozLoop: React.PropTypes.object.isRequired,
 | 
						|
      switchToInitialView: React.PropTypes.func.isRequired
 | 
						|
    },
 | 
						|
 | 
						|
    componentDidMount: function() {
 | 
						|
      this.initForm(this.props.contactFormData);
 | 
						|
    },
 | 
						|
 | 
						|
    getInitialState: function() {
 | 
						|
      return {
 | 
						|
        contact: null,
 | 
						|
        pristine: true,
 | 
						|
        name: "",
 | 
						|
        email: "",
 | 
						|
        tel: ""
 | 
						|
      };
 | 
						|
    },
 | 
						|
 | 
						|
    initForm: function(contact) {
 | 
						|
      let state = this.getInitialState();
 | 
						|
      // Test for an empty contact object
 | 
						|
      if (_.keys(contact).length > 0) {
 | 
						|
        state.contact = contact;
 | 
						|
        state.name = contact.name[0];
 | 
						|
        state.email = getPreferred(contact, "email").value;
 | 
						|
        state.tel = getPreferred(contact, "tel").value;
 | 
						|
      }
 | 
						|
 | 
						|
      this.setState(state);
 | 
						|
    },
 | 
						|
 | 
						|
    handleAcceptButtonClick: function() {
 | 
						|
      // Allow validity error indicators to be displayed.
 | 
						|
      this.setState({
 | 
						|
        pristine: false
 | 
						|
      });
 | 
						|
 | 
						|
      let emailInput = this.refs.email.getDOMNode();
 | 
						|
      let telInput = this.refs.tel.getDOMNode();
 | 
						|
      if (!this.refs.name.getDOMNode().checkValidity() ||
 | 
						|
          ((emailInput.required || emailInput.value) && !emailInput.checkValidity()) ||
 | 
						|
          ((telInput.required || telInput.value) && !telInput.checkValidity())) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      let contactsAPI = this.props.mozLoop.contacts;
 | 
						|
      switch (this.props.mode) {
 | 
						|
        case "edit":
 | 
						|
          this.state.contact.name[0] = this.state.name.trim();
 | 
						|
          setPreferred(this.state.contact, "email", this.state.email.trim());
 | 
						|
          setPreferred(this.state.contact, "tel", this.state.tel.trim());
 | 
						|
          contactsAPI.update(this.state.contact, err => {
 | 
						|
            if (err) {
 | 
						|
              throw err;
 | 
						|
            }
 | 
						|
          });
 | 
						|
          this.setState({
 | 
						|
            contact: null
 | 
						|
          });
 | 
						|
          break;
 | 
						|
        case "add":
 | 
						|
          var contact = {
 | 
						|
            id: this.props.mozLoop.generateUUID(),
 | 
						|
            name: [this.state.name.trim()],
 | 
						|
            email: [{
 | 
						|
              pref: true,
 | 
						|
              type: ["home"],
 | 
						|
              value: this.state.email.trim()
 | 
						|
            }],
 | 
						|
            category: ["local"]
 | 
						|
          };
 | 
						|
          var tel = this.state.tel.trim();
 | 
						|
          if (tel) {
 | 
						|
            contact.tel = [{
 | 
						|
              pref: true,
 | 
						|
              type: ["fxos"],
 | 
						|
              value: tel
 | 
						|
            }];
 | 
						|
          }
 | 
						|
          contactsAPI.add(contact, err => {
 | 
						|
            if (err) {
 | 
						|
              throw err;
 | 
						|
            }
 | 
						|
          });
 | 
						|
          break;
 | 
						|
      }
 | 
						|
 | 
						|
      this.props.switchToInitialView();
 | 
						|
    },
 | 
						|
 | 
						|
    handleCancelButtonClick: function() {
 | 
						|
      this.props.switchToInitialView();
 | 
						|
    },
 | 
						|
 | 
						|
    render: function() {
 | 
						|
      let cx = React.addons.classSet;
 | 
						|
      let phoneOrEmailRequired = !this.state.email && !this.state.tel;
 | 
						|
      let contactFormMode = "contact-form-mode-" + this.props.mode;
 | 
						|
      let contentAreaClassesLiteral = {
 | 
						|
        "content-area": true,
 | 
						|
        "contact-form": true
 | 
						|
      };
 | 
						|
      contentAreaClassesLiteral[contactFormMode] = true;
 | 
						|
      let contentAreaClasses = cx(contentAreaClassesLiteral);
 | 
						|
 | 
						|
      return (
 | 
						|
        React.createElement("div", {className: contentAreaClasses}, 
 | 
						|
          React.createElement("header", null, this.props.mode === "add"
 | 
						|
                   ? mozL10n.get("add_contact_title")
 | 
						|
                   : mozL10n.get("edit_contact_title")), 
 | 
						|
          React.createElement("div", {className: cx({"form-content-container": true})}, 
 | 
						|
            React.createElement("input", {className: cx({pristine: this.state.pristine}), 
 | 
						|
                   pattern: "\\s*\\S.*", 
 | 
						|
                   placeholder: mozL10n.get("contact_form_name_placeholder"), 
 | 
						|
                   ref: "name", 
 | 
						|
                   required: true, 
 | 
						|
                   type: "text", 
 | 
						|
                   valueLink: this.linkState("name")}), 
 | 
						|
            React.createElement("input", {className: cx({pristine: this.state.pristine}), 
 | 
						|
                   placeholder: mozL10n.get("contact_form_email_placeholder"), 
 | 
						|
                   ref: "email", 
 | 
						|
                   required: phoneOrEmailRequired, 
 | 
						|
                   type: "email", 
 | 
						|
                   valueLink: this.linkState("email")}), 
 | 
						|
            React.createElement("input", {className: cx({pristine: this.state.pristine}), 
 | 
						|
                   placeholder: mozL10n.get("contact_form_fxos_phone_placeholder"), 
 | 
						|
                   ref: "tel", 
 | 
						|
                   required: phoneOrEmailRequired, 
 | 
						|
                   type: "tel", 
 | 
						|
                   valueLink: this.linkState("tel")})
 | 
						|
          ), 
 | 
						|
          React.createElement(ButtonGroup, null, 
 | 
						|
            React.createElement(Button, {additionalClass: "button-cancel", 
 | 
						|
                    caption: mozL10n.get("cancel_button"), 
 | 
						|
                    onClick: this.handleCancelButtonClick}), 
 | 
						|
            React.createElement(Button, {additionalClass: "button-accept", 
 | 
						|
                    caption: this.props.mode === "add"
 | 
						|
                             ? mozL10n.get("add_contact_button")
 | 
						|
                             : mozL10n.get("edit_contact_done_button"), 
 | 
						|
                    onClick: this.handleAcceptButtonClick})
 | 
						|
          )
 | 
						|
        )
 | 
						|
      );
 | 
						|
    }
 | 
						|
  });
 | 
						|
 | 
						|
  return {
 | 
						|
    ContactDropdown: ContactDropdown,
 | 
						|
    ContactsList: ContactsList,
 | 
						|
    ContactDetail: ContactDetail,
 | 
						|
    ContactDetailsForm: ContactDetailsForm,
 | 
						|
    ContactsControllerView: ContactsControllerView,
 | 
						|
    _getPreferred: getPreferred,
 | 
						|
    _setPreferred: setPreferred
 | 
						|
  };
 | 
						|
})(_, document.mozL10n);
 |