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);
 | 
