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