'use strict'; /* global ActivityHandler */ /* global Cache */ /* global contacts */ /* global DeferredActions */ /* global fb */ /* global fbLoader */ /* global LazyLoader */ /* global MainNavigation */ /* global Loader */ /* global TAG_OPTIONS */ /* global utils */ /* global HeaderUI */ /* global Search */ /* global ContactsService */ /* exported COMMS_APP_ORIGIN */ /* exported SCALE_RATIO */ /* jshint nonew: false */ var COMMS_APP_ORIGIN = location.origin; // Scale ratio for different devices var SCALE_RATIO = window.devicePixelRatio || 1; var Contacts = (function() { var goToForm = function edit() { var transition = ActivityHandler.currentlyHandling ? 'activity-popup' : 'fade-in'; MainNavigation.go('view-contact-form', transition); }; var loadAsyncScriptsDeferred = {}; loadAsyncScriptsDeferred.promise = new Promise((resolve) => { loadAsyncScriptsDeferred.resolve = resolve; }); var settingsReady = false; var detailsReady = false; var formReady = false; var currentContact = {}, currentFbContact; var contactsList; var contactsDetails; var contactsForm; // Shows the edit form for the current contact being in an update activity // It receives an array of two elements with the facebook data && values function showEditForm(facebookData, params) { contactsForm.render(currentContact, goToForm, facebookData, params.fromUpdateActivity); } var checkUrl = function checkUrl() { var hasParams = window.location.hash.split('?'); var hash = hasParams[0]; var sectionId = hash.substr(1, hash.length) || ''; var params = hasParams.length > 1 ? utils.extractParams(hasParams[1]) : -1; switch (sectionId) { case 'view-contact-list': initContactsList(); break; case 'view-contact-details': initContactsList(); initDetails(function onInitDetails() { // At this point, a parameter is required if (params == -1) { console.error('Param missing'); return; } // If the parameter is an id, the corresponding contact is loaded // from the device. if ('id' in params) { var id = params.id; ContactsService.get(id, function onSuccess(savedContact) { currentContact = savedContact; contactsDetails.render(currentContact); MainNavigation.go(sectionId, 'right-left'); }, function onError() { console.error('Error retrieving contact'); }); // If mozContactParam is true, we know there is a mozContact // attached to the activity, so we render it using contacts details' // read only mode. This is used when we receive an activity to open // a given contact with allowSave set to false. } else if (params.mozContactParam) { var contact = ActivityHandler.mozContactParam; contactsDetails.render(contact, null, true); MainNavigation.go(sectionId, 'activity-popup'); } }); break; case 'view-contact-form': initForm(function onInitForm() { if (params.mozContactParam) { contactsForm.render(ActivityHandler.mozContactParam, goToForm); ActivityHandler.mozContactParam = null; } else if (params == -1 || !(params.id)) { contactsForm.render(params, goToForm); } else { // Editing existing contact if (params.id) { var id = params.id; ContactsService.get(id, function onSuccess(savedContact) { currentContact = savedContact; // Check if we have extra parameters to render if ('extras' in params) { addExtrasToContact(params.extras); } if (fb.isFbContact(savedContact)) { var fbContact = new fb.Contact(savedContact); var req = fbContact.getDataAndValues(); req.onsuccess = function() { showEditForm(req.result, params); }; req.onerror = function() { console.error('Error retrieving FB information'); showEditForm(null, params); }; } else { showEditForm(null, params); } }, function onError() { console.error('Error retrieving contact to be edited'); contactsForm.render(null, goToForm); }); } } }); break; case 'add-parameters': initContactsList(); initForm(function onInitForm() { MainNavigation.home(); if (ActivityHandler.currentlyHandling) { selectList(params, true); } }); break; case 'multiple-select-view': Loader.view('multiple_select', () => { MainNavigation.go('multiple-select-view', 'activity-popup'); }); break; case 'home': MainNavigation.home(); break; } }; var addExtrasToContact = function addExtrasToContact(extrasString) { try { var extras = JSON.parse(decodeURIComponent(extrasString)); for (var type in extras) { var extra = extras[type]; if (currentContact[type]) { if (Array.isArray(currentContact[type])) { var joinArray = currentContact[type].concat(extra); currentContact[type] = joinArray; } else { currentContact[type] = extra; } } else { currentContact[type] = Array.isArray(extra) ? extra : [extra]; } } } catch (e) { console.error('Extras malformed'); return null; } }; var onLocalized = function onLocalized() { init(); // We need to return the promise here for testing purposes return addAsyncScripts().then(() => { checkUrl(); if (!ActivityHandler.currentlyHandling || ActivityHandler.currentActivityIs(['pick', 'update'])) { initContactsList(); } else { // Unregister here to avoid un-necessary list operations. ContactsService.removeListener('contactchange', oncontactchange); } if (contactsList) { contactsList.initAlphaScroll(); } }); }; var loadDeferredActions = function loadDeferredActions() { window.removeEventListener('listRendered', loadDeferredActions); LazyLoader.load([ 'js/deferred_actions.js', '/contacts/js/fb_loader.js', '/contacts/js/fb/fb_init.js' ], function() { DeferredActions.execute(); }); }; var init = function init() { window.addEventListener('hashchange', checkUrl); window.addEventListener('listRendered', loadDeferredActions); /* Tell the audio channel manager that we want to adjust the "notification" * channel when the user presses the volumeup/volumedown buttons. */ if (navigator.mozAudioChannelManager) { navigator.mozAudioChannelManager.volumeControlChannel = 'notification'; } }; var initContactsList = function initContactsList() { if (contactsList) { return; } contactsList = contactsList || contacts.List; var list = document.getElementById('groups-list'); contactsList.init(list); getFirstContacts(); contactsList.initAlphaScroll(); contactsList.handleClick(contactListClickHandler); ActivityHandler.isCancelable().then(isCancelable => { HeaderUI.updateHeader(isCancelable); }); }; var contactListClickHandler = function originalHandler(id) { initDetails(function onDetailsReady() { ContactsService.get(id, function findCb(contact, fbContact) { currentContact = contact; currentFbContact = fbContact; if (ActivityHandler.currentActivityIsNot(['import'])) { if (ActivityHandler.currentActivityIs(['pick'])) { ActivityHandler.dataPickHandler(currentFbContact || currentContact); } return; } contactsDetails.render(currentContact, currentFbContact); if (window.Search && Search.isInSearchMode()) { MainNavigation.go('view-contact-details', 'go-deeper-search'); } else { MainNavigation.go('view-contact-details', 'go-deeper'); } }); }); }; var updateContactDetail = function updateContactDetail(id) { ContactsService.get(id, function findCallback(contact) { currentContact = contact; contactsDetails.render(currentContact); }); }; var selectList = function selectList(params, fromUpdateActivity) { HeaderUI.hideAddButton(); contactsList.clearClickHandlers(); contactsList.handleClick(function addToContactHandler(id) { var data = {}; if (params.hasOwnProperty('tel')) { var phoneNumber = params.tel; data.tel = [{ 'value': phoneNumber, 'carrier': null, 'type': [TAG_OPTIONS['phone-type'][0].type] }]; } if (params.hasOwnProperty('email')) { var email = params.email; data.email = [{ 'value': email, 'type': [TAG_OPTIONS['email-type'][0].type] }]; } var hash = '#view-contact-form?extras=' + encodeURIComponent(JSON.stringify(data)) + '&id=' + id; if (fromUpdateActivity) { hash += '&fromUpdateActivity=1'; } window.location.hash = hash; }); }; var handleBack = function handleBack(cb) { MainNavigation.back(cb); }; var handleCancel = function handleCancel() { //If in an activity, cancel it if (ActivityHandler.currentlyHandling) { ActivityHandler.postCancel(); MainNavigation.home(); } else { handleBack(function() { // TODO: remove all interaction with detail.js when it works // as an independent view if (MainNavigation.currentView() === 'view-contact-details') { contactsDetails.startNFC(currentContact); } }); } }; var showAddContact = function showAddContact() { showForm(); }; var loadFacebook = function loadFacebook(callback) { LazyLoader.load([ '/contacts/js/fb_loader.js', '/contacts/js/fb/fb_init.js' ], () => { if (!fbLoader.loaded) { fb.init(function onInitFb() { window.addEventListener('facebookLoaded', function onFbLoaded() { window.removeEventListener('facebookLoaded', onFbLoaded); callback(); }); fbLoader.load(); }); } else { callback(); } }); }; var initForm = function c_initForm(callback) { if (formReady) { callback(); } else { initDetails(function onDetails() { LazyLoader.load([ '/shared/js/contacts/import/utilities/misc.js', '/shared/js/contacts/utilities/image_thumbnail.js', '/contacts/js/match_service.js'], function() { Loader.view('Form', function viewLoaded() { formReady = true; contactsForm = contacts.Form; contactsForm.init(TAG_OPTIONS); callback(); }); }); }); } }; var initSettings = function c_initSettings(callback) { if (settingsReady) { callback(); } else { Loader.view('Settings', function viewLoaded() { LazyLoader.load(['/contacts/js/utilities/sim_dom_generator.js', '/contacts/js/utilities/normalizer.js', '/shared/js/contacts/import/utilities/misc.js', '/shared/js/mime_mapper.js', '/shared/js/contacts/import/utilities/vcard_parser.js', '/contacts/js/utilities/icc_handler.js', '/shared/js/contacts/import/utilities/sdcard.js', '/shared/elements/gaia_switch/script.js', '/shared/js/date_time_helper.js', '/shared/js/contacts/import/import_status_data.js'], function() { settingsReady = true; contacts.Settings.init(); callback(); }); }); } }; var initDetails = function c_initDetails(callback) { if (detailsReady) { callback(); } else { Loader.view('Details', function viewLoaded() { LazyLoader.load( ['/shared/js/contacts/import/utilities/misc.js', '/dialer/js/telephony_helper.js', '/shared/js/contacts/sms_integration.js', '/shared/js/contacts/contacts_buttons.js', '/contacts/js/match_service.js', '/contacts/js/utilities/mozContact.js'], function() { detailsReady = true; contactsDetails = contacts.Details; contactsDetails.init(); callback(); }); }); } }; var showForm = function c_showForm(edit, contact) { currentContact = contact || currentContact; initForm(function onInit() { doShowForm(edit); }); }; var doShowForm = function c_doShowForm(edit) { var contact = edit ? currentContact : null; if (contact && fb.isFbContact(contact)) { var fbContact = new fb.Contact(contact); var req = fbContact.getDataAndValues(); req.onsuccess = function() { contactsForm.render(contact, goToForm, req.result); }; req.onerror = function() { contactsForm.render(contact, goToForm); }; } else { contactsForm.render(contact, goToForm); } }; var setCurrent = function c_setCurrent(contact) { currentContact = contact; if (contacts.Details) { contacts.Details.setContact(contact); } }; var showOverlay = function c_showOverlay(messageId, progressClass, textId) { var out = utils.overlay.show(messageId, progressClass, textId); // When we are showing the overlay we are often performing other // significant work, such as importing. While performing this work // it would be nice to avoid the overhead of any accidental reflows // due to touching the list DOM. For example, importing incrementally // adds contacts to the list which triggers many reflows. Therefore, // minimize this impact by hiding the list while we are showing the // overlay. contacts.List.hide(); return out; }; var hideOverlay = function c_hideOverlay() { Loader.utility('Overlay', function _loaded() { contacts.List.show(); utils.overlay.hide(); }); }; var showSettings = function showSettings() { initSettings(function onSettingsReady() { // The number of FB Friends has to be recalculated contacts.Settings.refresh(); MainNavigation.go('view-settings', 'fade-in'); }); }; var stopPropagation = function stopPropagation(evt) { evt.preventDefault(); }; var enterSearchMode = function enterSearchMode(evt) { Loader.view('Search', function viewLoaded() { contacts.List.initSearch(function onInit() { Search.enterSearchMode(evt); }); }); }; var initEventListeners = function initEventListener() { // Definition of elements and handlers utils.listeners.add({ '#add-contact-button': showAddContact, '#settings-button': showSettings, // Settings related '#search-start': [ { event: 'click', handler: enterSearchMode } ], // For screen reader users '#search-start > input': [ { event: 'focus', handler: enterSearchMode } ], 'button[type="reset"]': stopPropagation }); }; var getFirstContacts = function c_getFirstContacts() { var onerror = function() { console.error('Error getting first contacts'); }; contactsList = contactsList || contacts.List; contactsList.getAllContacts(onerror); }; var addAsyncScripts = function addAsyncScripts() { var lazyLoadFiles = [ '/shared/js/contacts/utilities/templates.js', '/shared/js/contacts/contacts_shortcuts.js', '/contacts/js/contacts_tag.js', '/contacts/js/tag_options.js', '/contacts/js/loader.js', '/shared/js/text_normalizer.js', '/shared/js/contacts/import/utilities/status.js', '/shared/js/contacts/utilities/dom.js', '/shared/js/confirm.js', document.getElementById('confirmation-message') ]; LazyLoader.load(lazyLoadFiles, function() { loadAsyncScriptsDeferred.resolve(); }); return loadAsyncScriptsDeferred.promise; }; var pendingChanges = {}; // This function is called when we finish a oncontactchange operation to // remove the op of the pending changes and check if we need to apply more // changes request over the same id. var checkPendingChanges = function checkPendingChanges(id) { var changes = pendingChanges[id]; if (!changes) { return; } pendingChanges[id].shift(); if (pendingChanges[id].length >= 1) { performOnContactChange(pendingChanges[id][0]); } }; var oncontactchange = function oncontactchange(event) { if (typeof pendingChanges[event.contactID] !== 'undefined') { pendingChanges[event.contactID].push({ contactID: event.contactID, reason: event.reason }); } else { pendingChanges[event.contactID] = [{ contactID: event.contactID, reason: event.reason }]; } // If there is already a pending request, don't do anything, // just wait to finish it in order if (pendingChanges[event.contactID].length > 1) { return; } performOnContactChange(event); }; ContactsService.addListener('contactchange', oncontactchange); var performOnContactChange = function performOnContactChange(event) { // To be on the safe side for now we evict the cache everytime a // contact change event is received. In the future, we may want to check // if the change affects the cache or not, so we avoid evicting it when // is not needed. Cache.evict(); initContactsList(); var currView = MainNavigation.currentView(); switch (event.reason) { case 'update': if (currView == 'view-contact-details' && currentContact != null && currentContact.id == event.contactID) { ContactsService.get(event.contactID, function success(contact, enrichedContact) { currentContact = contact; if (contactsDetails) { contactsDetails.render(currentContact, enrichedContact); } if (contactsList) { contactsList.refresh(enrichedContact || currentContact, checkPendingChanges, event.reason); } notifyContactChanged(event.contactID, event.reason); }); } else { refreshContactInList(event.contactID); } break; case 'create': refreshContactInList(event.contactID); break; case 'remove': if (currentContact != null && currentContact.id == event.contactID && (currView == 'view-contact-details' || currView == 'view-contact-form')) { MainNavigation.home(); } contactsList.remove(event.contactID, event.reason); currentContact = {}; checkPendingChanges(event.contactID); notifyContactChanged(event.contactID, event.reason); break; } }; // Refresh a contact in the list, and notifies of contact // changed to possible listeners. function refreshContactInList(id) { contactsList.refresh(id, function() { notifyContactChanged(id); checkPendingChanges(id); }); } // Send a custom event when we know that a contact changed and // the contact list was updated. // Used internally in places where the contact list is a reference function notifyContactChanged(id, reason) { document.dispatchEvent(new CustomEvent('contactChanged', { detail: { contactID: id, reason: reason } })); } var close = function close() { window.removeEventListener('localized', initContacts); }; var initContacts = function initContacts(evt) { initEventListeners(); HeaderUI.setNormalHeader(); utils.PerformanceHelper.contentInteractive(); utils.PerformanceHelper.chromeInteractive(); window.setTimeout(Contacts && Contacts.onLocalized); // Vulpes embeds each app in its own permission-checked frame. if (window.navigator.mozSetMessageHandler && (window.self == window.top || window.VulpesCompat)) { LazyLoader.load(['/shared/js/contacts/import/utilities/misc.js', '/shared/js/contacts/import/utilities/vcard_reader.js', '/shared/js/contacts/import/utilities/vcard_parser.js'], function() { var actHandler = ActivityHandler.handle.bind(ActivityHandler); window.navigator.mozSetMessageHandler('activity', actHandler); }); } document.addEventListener('visibilitychange', function visibility(e) { if (document.hidden === false && MainNavigation.currentView() === 'view-settings') { Loader.view('Settings', function viewLoaded() { contacts.Settings.updateTimestamps(); }); } }); }; LazyLoader.load('/shared/js/l10n.js', () => { navigator.mozL10n.once(() => { initContacts(); }); navigator.mozL10n.ready(() => { Cache.maybeEvict(); }); }); window.addEventListener('DOMContentLoaded', function onLoad() { window.removeEventListener('DOMContentLoaded', onLoad); }); return { 'goBack' : handleBack, 'cancel': handleCancel, 'showForm': showForm, 'setCurrent': setCurrent, 'onLocalized': onLocalized, 'init': init, 'showOverlay': showOverlay, 'hideOverlay': hideOverlay, 'showContactDetail': contactListClickHandler, 'updateContactDetail': updateContactDetail, 'loadFacebook': loadFacebook, 'close': close, get asyncScriptsLoaded() { return loadAsyncScriptsDeferred.promise; } }; })();