Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
85
gaia/apps/communications/contacts/js/action_menu.js
Executable file
85
gaia/apps/communications/contacts/js/action_menu.js
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
/* exported ActionMenu */
|
||||
'use strict';
|
||||
|
||||
function ActionMenu(title) {
|
||||
var data;
|
||||
var contactActionMenu;
|
||||
var listContainer;
|
||||
var btnCancel;
|
||||
var titleId = title;
|
||||
|
||||
function init() {
|
||||
contactActionMenu = document.getElementById('action-menu');
|
||||
// Prevent submit in the form
|
||||
contactActionMenu.addEventListener('submit', function(evt) {
|
||||
evt.preventDefault();
|
||||
});
|
||||
|
||||
if (titleId) {
|
||||
contactActionMenu.querySelector('#org-title').setAttribute(
|
||||
'data-l10n-id', titleId);
|
||||
}
|
||||
listContainer = document.getElementById('value-menu');
|
||||
|
||||
data = {
|
||||
list: []
|
||||
};
|
||||
}
|
||||
|
||||
this.show = function() {
|
||||
render(this);
|
||||
contactActionMenu.classList.remove('hide');
|
||||
};
|
||||
|
||||
this.hide = function() {
|
||||
contactActionMenu.classList.add('hide');
|
||||
emptyList();
|
||||
listContainer.innerHTML = '';
|
||||
};
|
||||
|
||||
function render(self) {
|
||||
for (var i = 0, l = data.list.length; i < l; i++) {
|
||||
var button = document.createElement('button');
|
||||
navigator.mozL10n.setAttributes(button,
|
||||
data.list[i].labelL10n.id,
|
||||
data.list[i].labelL10n.args);
|
||||
|
||||
// Set callback function on each button element.
|
||||
var callback = data.list[i].callback;
|
||||
if (callback) {
|
||||
button.addEventListener('click', callback, false);
|
||||
}
|
||||
|
||||
listContainer.appendChild(button);
|
||||
}
|
||||
|
||||
btnCancel = document.createElement('button');
|
||||
btnCancel.setAttribute('data-l10n-id', 'cancel');
|
||||
|
||||
btnCancel.addEventListener('click', function() {
|
||||
self.hide();
|
||||
});
|
||||
|
||||
var lastButtonWrapper = document.createElement('span');
|
||||
lastButtonWrapper.className = 'last-button-container';
|
||||
listContainer.appendChild(lastButtonWrapper);
|
||||
lastButtonWrapper.appendChild(btnCancel);
|
||||
}
|
||||
|
||||
function emptyList() {
|
||||
data.list = [];
|
||||
}
|
||||
|
||||
this.addToList = function (labelL10n, callback) {
|
||||
var self = this;
|
||||
data.list.push({
|
||||
labelL10n: labelL10n,
|
||||
callback: function() {
|
||||
callback();
|
||||
self.hide();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
init();
|
||||
}
|
||||
370
gaia/apps/communications/contacts/js/activities.js
Executable file
370
gaia/apps/communications/contacts/js/activities.js
Executable file
|
|
@ -0,0 +1,370 @@
|
|||
/* globals ConfirmDialog, Contacts, LazyLoader, utils, ActionMenu, HeaderUI,
|
||||
ContactToVcardBlob, VcardFilename, VcardActivityHandler, MainNavigation */
|
||||
/* exported ActivityHandler */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Per RFC 6350, text/vcard is the canonical MIME media type for vCards, but
|
||||
* there are also deprecated types as well. Whenever we disambiguate what an
|
||||
* activity is requesting based on its MIME media type, we need to check if it
|
||||
* is any of these, and not just text/vcard.
|
||||
*/
|
||||
const VCARD_MIME_TYPES = [
|
||||
'text/vcard',
|
||||
'text/x-vcard',
|
||||
'text/directory'
|
||||
];
|
||||
|
||||
var ActivityHandler = {
|
||||
_currentActivity: null,
|
||||
|
||||
_launchedAsInlineActivity: (window.location.search == '?pick'),
|
||||
|
||||
mozContactParam: null,
|
||||
|
||||
_actionMenu: null,
|
||||
|
||||
get currentlyHandling() {
|
||||
return !!this._currentActivity;
|
||||
},
|
||||
|
||||
get activityName() {
|
||||
if (!this._currentActivity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._currentActivity.source.name;
|
||||
},
|
||||
|
||||
get activityDataType() {
|
||||
if (!this._currentActivity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._currentActivity.source.data.type;
|
||||
},
|
||||
|
||||
get activityData() {
|
||||
if (!this._currentActivity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._currentActivity.source.data;
|
||||
},
|
||||
|
||||
get activityContactProperties() {
|
||||
if (!this._currentActivity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._currentActivity.source.data.contactProperties;
|
||||
},
|
||||
|
||||
/* checks first if we are handling an activity, then if it is
|
||||
* of the same type of any of the items from the list provided.
|
||||
* @param list Array with types of activities to be checked
|
||||
*/
|
||||
currentActivityIs: function(list) {
|
||||
return this.currentlyHandling && list.indexOf(this.activityName) !== -1;
|
||||
},
|
||||
|
||||
/* checks first if we are handling an activity, then checks that
|
||||
* it is NOT of the same type of any of the items from the list provided.
|
||||
* @param list Array with types of activities to be checked
|
||||
*/
|
||||
currentActivityIsNot: function(list) {
|
||||
return this.currentlyHandling && list.indexOf(this.activityName) === -1;
|
||||
},
|
||||
|
||||
isCancelable: function() {
|
||||
return new Promise(resolve => {
|
||||
resolve(!!this.currentlyHandling);
|
||||
});
|
||||
},
|
||||
|
||||
launch_activity: function ah_launch(activity, action) {
|
||||
if (this._launchedAsInlineActivity) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentActivity = activity;
|
||||
this.isCancelable().then(isCancelable => {
|
||||
HeaderUI.updateHeader(isCancelable);
|
||||
});
|
||||
|
||||
var hash = action;
|
||||
var param, params = [];
|
||||
if (activity.source &&
|
||||
activity.source.data &&
|
||||
activity.source.data.params) {
|
||||
var originalParams = activity.source.data.params;
|
||||
for (var i in originalParams) {
|
||||
param = originalParams[i];
|
||||
params.push(i + '=' + param);
|
||||
}
|
||||
hash += '?' + params.join('&');
|
||||
}
|
||||
document.location.hash = hash;
|
||||
},
|
||||
|
||||
handle: function ah_handle(activity) {
|
||||
const VCARD_DEPS = '/contacts/js/activities_vcard.js';
|
||||
switch (activity.source.name) {
|
||||
case 'new':
|
||||
this.launch_activity(activity, 'view-contact-form');
|
||||
break;
|
||||
case 'import':
|
||||
case 'open':
|
||||
LazyLoader.load(VCARD_DEPS, () => {
|
||||
VcardActivityHandler.handle(activity, this);
|
||||
});
|
||||
break;
|
||||
case 'update':
|
||||
this.launch_activity(activity, 'add-parameters');
|
||||
break;
|
||||
case 'pick':
|
||||
if (!this._launchedAsInlineActivity) {
|
||||
return;
|
||||
}
|
||||
this._currentActivity = activity;
|
||||
this.isCancelable().then(isCancelable => {
|
||||
HeaderUI.updateHeader(isCancelable);
|
||||
});
|
||||
MainNavigation.home();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
importContactsFromFile: function ah_importContactFromVcard(activity) {
|
||||
var self = this;
|
||||
if (activity.source &&
|
||||
activity.source.data &&
|
||||
activity.source.data.blob) {
|
||||
LazyLoader.load([
|
||||
document.querySelector('#loading-overlay'),
|
||||
'/shared/js/contacts/import/utilities/import_from_vcard.js',
|
||||
'/shared/js/contacts/import/utilities/overlay.js'
|
||||
], function loaded() {
|
||||
Contacts.loadFacebook(function() {
|
||||
utils.importFromVcard(activity.source.data.blob,
|
||||
function imported(numberOfContacts, id) {
|
||||
if (numberOfContacts === 1) {
|
||||
activity.source.data.params = {id: id};
|
||||
self.launch_activity(activity, 'view-contact-details');
|
||||
} else {
|
||||
self.launch_activity(activity, 'view-contact-list');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this._currentActivity.postError('wrong parameters');
|
||||
this._currentActivity = null;
|
||||
}
|
||||
},
|
||||
|
||||
dataPickHandler: function ah_dataPickHandler(theContact) {
|
||||
var type, dataSet, noDataStr;
|
||||
var result = {};
|
||||
var self = this;
|
||||
|
||||
// Keeping compatibility with previous implementation. If
|
||||
// we want to get the full contact, just pass the parameter
|
||||
// 'fullContact' equal true.
|
||||
if (this.activityDataType === 'webcontacts/contact' &&
|
||||
this.activityData.fullContact === true) {
|
||||
result = utils.misc.toMozContact(theContact);
|
||||
this.postPickSuccess(result);
|
||||
return;
|
||||
}
|
||||
|
||||
// Was this a request for vCard export as a blob? Check all supported MIME
|
||||
// types.
|
||||
var isVcardDataType = VCARD_MIME_TYPES.some((mime) => {
|
||||
return this.activityDataType.indexOf(mime) !== -1;
|
||||
});
|
||||
|
||||
if (isVcardDataType) {
|
||||
// Normalize the type to text/vcard so other places that check MIME types
|
||||
// (ex: the Facebook guards) experience a consistent MIME type.
|
||||
this._currentActivity.source.data.type = 'text/vcard';
|
||||
LazyLoader.load([
|
||||
'/shared/js/text_normalizer.js',
|
||||
'/shared/js/contact2vcard.js',
|
||||
'/shared/js/setImmediate.js'
|
||||
], function lvcard() {
|
||||
ContactToVcardBlob([theContact], function blobReady(vcardBlob) {
|
||||
VcardFilename(theContact).then(filename => {
|
||||
self.postPickSuccess({
|
||||
name: filename,
|
||||
blob: vcardBlob
|
||||
});
|
||||
});
|
||||
}, {
|
||||
// Some MMS gateways prefer this MIME type for vcards
|
||||
type: 'text/x-vcard'
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.activityDataType) {
|
||||
case 'webcontacts/tel':
|
||||
type = 'contact';
|
||||
dataSet = theContact.tel;
|
||||
noDataStr = 'no_contact_phones';
|
||||
break;
|
||||
case 'webcontacts/contact':
|
||||
type = 'number';
|
||||
dataSet = theContact.tel;
|
||||
noDataStr = 'no_contact_phones';
|
||||
break;
|
||||
case 'webcontacts/email':
|
||||
type = 'email';
|
||||
dataSet = theContact.email;
|
||||
noDataStr = 'no_contact_email';
|
||||
break;
|
||||
case 'webcontacts/select':
|
||||
type = 'select';
|
||||
var data = [];
|
||||
if (this.activityContactProperties.indexOf('tel') !== -1) {
|
||||
if (theContact.tel && theContact.tel.length) {
|
||||
data = data.concat(theContact.tel);
|
||||
}
|
||||
}
|
||||
if (this.activityContactProperties.indexOf('email') !== -1) {
|
||||
if (theContact.email && theContact.email.length) {
|
||||
data = data.concat(theContact.email);
|
||||
}
|
||||
}
|
||||
|
||||
dataSet = data;
|
||||
noDataStr = 'no_contact_data_available';
|
||||
break;
|
||||
}
|
||||
var hasData = dataSet && dataSet.length;
|
||||
var numOfData = hasData ? dataSet.length : 0;
|
||||
|
||||
|
||||
result.name = theContact.name;
|
||||
switch (numOfData) {
|
||||
case 0:
|
||||
// If no required type of data
|
||||
var dismiss = {
|
||||
title: 'ok',
|
||||
callback: function() {
|
||||
ConfirmDialog.hide();
|
||||
}
|
||||
};
|
||||
ConfirmDialog.show(null, noDataStr, dismiss);
|
||||
break;
|
||||
case 1:
|
||||
// if one required type of data
|
||||
if (this.activityDataType == 'webcontacts/tel' ||
|
||||
this.activityDataType == 'webcontacts/select') {
|
||||
result = this.pickContactsResult(theContact);
|
||||
} else {
|
||||
result[type] = dataSet[0].value;
|
||||
}
|
||||
|
||||
this.postPickSuccess(result);
|
||||
break;
|
||||
default:
|
||||
// if more than one required type of data
|
||||
LazyLoader.load('/contacts/js/action_menu.js', function() {
|
||||
if (!self._actionMenu) {
|
||||
self._actionMenu = new ActionMenu();
|
||||
} else {
|
||||
// To be sure that the action menu is empty
|
||||
self._actionMenu.hide();
|
||||
}
|
||||
|
||||
var itemData;
|
||||
var capture = function(itemData) {
|
||||
return function() {
|
||||
if (self.activityDataType == 'webcontacts/tel' ||
|
||||
self.activityDataType == 'webcontacts/select') {
|
||||
result = self.pickContactsResult(theContact, itemData);
|
||||
} else {
|
||||
result[type] = itemData;
|
||||
}
|
||||
self._actionMenu.hide();
|
||||
self.postPickSuccess(result);
|
||||
};
|
||||
};
|
||||
for (var i = 0, l = dataSet.length; i < l; i++) {
|
||||
itemData = dataSet[i].value;
|
||||
var carrier = dataSet[i].carrier || '';
|
||||
self._actionMenu.addToList(
|
||||
{
|
||||
id: 'pick_destination',
|
||||
args: {destination: itemData, carrier: carrier}
|
||||
},
|
||||
capture(itemData)
|
||||
);
|
||||
}
|
||||
self._actionMenu.show();
|
||||
});
|
||||
} // switch
|
||||
},
|
||||
|
||||
pickContactsResult:
|
||||
function ah_pickContactsResult(theContact, itemData) {
|
||||
var pickResult = {};
|
||||
var contact = utils.misc.toMozContact(theContact);
|
||||
|
||||
if (this.activityDataType == 'webcontacts/tel') {
|
||||
pickResult = contact;
|
||||
|
||||
if (itemData) {
|
||||
pickResult.tel = this.filterDestinationForActivity(
|
||||
itemData, pickResult.tel);
|
||||
}
|
||||
} else if (this.activityDataType == 'webcontacts/select') {
|
||||
pickResult.contact = contact;
|
||||
|
||||
if (!itemData) {
|
||||
pickResult.select = pickResult.contact.tel;
|
||||
|
||||
if (!pickResult.select || !pickResult.select.length) {
|
||||
pickResult.select = pickResult.contact.email;
|
||||
}
|
||||
} else {
|
||||
pickResult.select = this.filterDestinationForActivity(
|
||||
itemData, pickResult.contact.tel);
|
||||
|
||||
if (!pickResult.select || !pickResult.select.length) {
|
||||
pickResult.select = this.filterDestinationForActivity(
|
||||
itemData, pickResult.contact.email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pickResult;
|
||||
},
|
||||
|
||||
filterDestinationForActivity:
|
||||
function ah_filterDestinationForActivity(itemData, dataSet) {
|
||||
return dataSet.filter(function isSamePhone(item) {
|
||||
return item.value == itemData;
|
||||
});
|
||||
},
|
||||
|
||||
postNewSuccess: function ah_postNewSuccess(contact) {
|
||||
this._currentActivity.postResult({ contact: contact });
|
||||
this._currentActivity = null;
|
||||
},
|
||||
|
||||
postPickSuccess: function ah_postPickSuccess(result) {
|
||||
this._currentActivity.postResult(result);
|
||||
this._currentActivity = null;
|
||||
},
|
||||
|
||||
postCancel: function ah_postCancel() {
|
||||
this._currentActivity.postError('canceled');
|
||||
this._currentActivity = null;
|
||||
}
|
||||
};
|
||||
66
gaia/apps/communications/contacts/js/activities_vcard.js
Executable file
66
gaia/apps/communications/contacts/js/activities_vcard.js
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
'use strict';
|
||||
/* global LazyLoader, VCardReader, Contacts */
|
||||
/* exported VcardActivityHandler */
|
||||
|
||||
var VcardActivityHandler = (function() {
|
||||
var handle = function(activity, parentHandler) {
|
||||
const DEPENDENCIES = '/shared/js/contacts/import/utilities/vcard_reader.js';
|
||||
LazyLoader.load(DEPENDENCIES, function () {
|
||||
getvCardReader(activity.source.data.blob).then(readvCard, (err) => {
|
||||
console.error('Error while getting vCard reader:', err);
|
||||
Contacts.cancel();
|
||||
});
|
||||
});
|
||||
|
||||
function getvCardReader(blob) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var fileReader = new FileReader();
|
||||
fileReader.readAsText(blob);
|
||||
fileReader.onloadend = function() {
|
||||
resolve(new VCardReader(fileReader.result));
|
||||
};
|
||||
fileReader.onerror = function() {
|
||||
console.error('The blob could not be read as a file.');
|
||||
reject(fileReader.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function readvCard(vCardReader) {
|
||||
var cursor = vCardReader.getAll();
|
||||
cursor.onsuccess = function(event) {
|
||||
var contact = event.target.result;
|
||||
render([contact], activity, cursor);
|
||||
};
|
||||
}
|
||||
|
||||
function getFileName(path) {
|
||||
var barIndex = path.lastIndexOf('/');
|
||||
if (barIndex > -1) {
|
||||
path = path.substring(barIndex + 1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function render(contacts, activity, cursor) {
|
||||
parentHandler.launch_activity(activity, 'multiple-select-view');
|
||||
const DEPENDENCIES = [
|
||||
'/contacts/js/views/multiple_select.js'
|
||||
];
|
||||
LazyLoader.load(DEPENDENCIES, function() {
|
||||
var name;
|
||||
if (activity.source.data.filename) {
|
||||
name = getFileName(activity.source.data.filename);
|
||||
} else {
|
||||
name = activity.source.data.src;
|
||||
}
|
||||
Contacts.MultipleSelect.init();
|
||||
Contacts.MultipleSelect.render(contacts, cursor, name);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handle: handle
|
||||
};
|
||||
})();
|
||||
90
gaia/apps/communications/contacts/js/bootstrap.js
vendored
Executable file
90
gaia/apps/communications/contacts/js/bootstrap.js
vendored
Executable file
|
|
@ -0,0 +1,90 @@
|
|||
/* global Cache */
|
||||
/* global LazyLoader */
|
||||
/* global utils */
|
||||
|
||||
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
/** Script loader **/
|
||||
|
||||
function loadScripts() {
|
||||
var dependencies = [
|
||||
'/contacts/services/contacts.js',
|
||||
'/contacts/js/activities.js',
|
||||
'/shared/js/contacts/utilities/event_listeners.js',
|
||||
'/contacts/js/navigation.js',
|
||||
'/contacts/js/main_navigation.js',
|
||||
'/contacts/js/views/list.js',
|
||||
'/contacts/js/header_ui.js'
|
||||
];
|
||||
|
||||
// If the cache is enabled, we push lazy loading l10n to the extreme,
|
||||
// cause we will be applying the transalations manually from the cached
|
||||
// content.
|
||||
// Otherwise, we load the l10n scripts along with the rest of the JS
|
||||
// scripts. This will avoid the non localized text to appear in the screen.
|
||||
if (!Cache.active) {
|
||||
dependencies.push('/shared/js/l10n.js');
|
||||
}
|
||||
|
||||
LazyLoader.load(dependencies, () => {
|
||||
['/shared/js/async_storage.js',
|
||||
'/shared/js/contacts/import/utilities/config.js',
|
||||
'/contacts/js/utilities/extract_params.js',
|
||||
'/contacts/js/utilities/cookie.js',
|
||||
'/shared/js/contact_photo_helper.js'].forEach((src) => {
|
||||
var scriptNode = document.createElement('script');
|
||||
scriptNode.src = src;
|
||||
scriptNode.setAttribute('defer', true);
|
||||
document.head.appendChild(scriptNode);
|
||||
});
|
||||
return LazyLoader.load('/contacts/js/contacts.js');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap process
|
||||
* -----------------
|
||||
* In order to provide an acceptable start up time, the contacts app
|
||||
* keeps a cached version of the list view HTML in localStorage. We
|
||||
* initially load only the scripts that are needed to recover the cached
|
||||
* HTML from localStorage and to apply it as part of the contacts list view.
|
||||
* Once we have done this and have shown the cached version of the list in
|
||||
* the screen, we load the rest of required JS files and continue the normal
|
||||
* start up process.
|
||||
*
|
||||
* For now, the cache is evicted in the following scenarios:
|
||||
* - oncontactchange event (addition, edition or deletion of contacts).
|
||||
* - ICE contacts enabled/disabled.
|
||||
* - Facebook import enabled/disabled/sync.
|
||||
* - Import from external source (Gmail, Hotmail, Sdcard...).
|
||||
* - Favorites added/removed.
|
||||
* - Change in the way we display contacts (order by last name).
|
||||
* - Locale change (from RTL to LTR and viceversa).
|
||||
*/
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function ondomloaded() {
|
||||
utils.PerformanceHelper.domLoaded();
|
||||
window.removeEventListener('DOMContentLoaded', ondomloaded);
|
||||
|
||||
var firstPaintFired = false;
|
||||
|
||||
Cache.apply('firstChunk').then(() => {
|
||||
// Once the first chunk is applied correctly, display the
|
||||
// list panel and fire the visually complete event.
|
||||
document.getElementById('view-contacts-list').classList.add('current');
|
||||
utils.PerformanceHelper.visuallyComplete();
|
||||
firstPaintFired = true;
|
||||
});
|
||||
|
||||
window.onload = () => {
|
||||
if (!firstPaintFired) {
|
||||
utils.PerformanceHelper.visuallyComplete();
|
||||
}
|
||||
loadScripts();
|
||||
};
|
||||
});
|
||||
|
||||
})(window);
|
||||
454
gaia/apps/communications/contacts/js/cache.js
Executable file
454
gaia/apps/communications/contacts/js/cache.js
Executable file
|
|
@ -0,0 +1,454 @@
|
|||
'use strict';
|
||||
|
||||
(function() {
|
||||
// Ideally we should store this value in the configuration file.
|
||||
// However, that means that we need to load two more script files and
|
||||
// read directly from the config.json file or from the cookie storing
|
||||
// the configuration data. This steals us ~150ms of startup time, so
|
||||
// we chose to hard code this value instead.
|
||||
const CACHE_ENABLED = true;
|
||||
const FIRST_CHUNK = 'firstChunk';
|
||||
|
||||
const l10nStrings = {
|
||||
'title': {
|
||||
query: 'gaia-header h1#app-title[data-l10n-id="contacts"]',
|
||||
container: 'textContent'
|
||||
},
|
||||
'search': {
|
||||
query: 'input[data-l10n-id="search-contact"]',
|
||||
container: 'placeholder'
|
||||
}
|
||||
};
|
||||
|
||||
var _observers = {};
|
||||
|
||||
function addObserver(aMessage, aCallback) {
|
||||
if (typeof aCallback !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_observers[aMessage]) {
|
||||
_observers[aMessage] = [];
|
||||
}
|
||||
_observers[aMessage].push(aCallback);
|
||||
}
|
||||
|
||||
function notifyObservers(aMessage, aData) {
|
||||
var observers = _observers[aMessage];
|
||||
if (!observers) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var callback in observers) {
|
||||
if (observers[callback]) {
|
||||
observers[callback](aData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// So far we only cache the first chunk of contacts, but we may want to
|
||||
// extend the cache to a bigger or even the whole list in the future.
|
||||
var _caches = new Map();
|
||||
_caches.set(FIRST_CHUNK, {
|
||||
containerId: 'groups-list',
|
||||
active: false
|
||||
});
|
||||
|
||||
var _cachedContacts;
|
||||
var _cachedFavorites;
|
||||
var _cachedHeaders;
|
||||
var _cacheEvictionTimer;
|
||||
|
||||
function setCache(aCache) {
|
||||
if (!CACHE_ENABLED || !aCache.id ||
|
||||
!aCache.content || !aCache.lastOrderString ||
|
||||
!_caches.has(aCache.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var l10n = {};
|
||||
Object.keys(l10nStrings).forEach(key => {
|
||||
var string = l10nStrings[key];
|
||||
var selector = document.querySelector(string.query);
|
||||
if (!selector || !selector[string.container]) {
|
||||
return;
|
||||
}
|
||||
l10n[key] = selector[string.container];
|
||||
});
|
||||
|
||||
localStorage.setItem(aCache.id, JSON.stringify({
|
||||
content: aCache.content,
|
||||
languageDirection: navigator.mozL10n.language.direction,
|
||||
languageCode: navigator.mozL10n.language.code,
|
||||
lastOrderString: aCache.lastOrderString,
|
||||
updated: Date.now(),
|
||||
// Because we lazy load l10n.js quite late, we need to cache the
|
||||
// localized strings to avoid showing the unlocalized text during
|
||||
// the start up process.
|
||||
l10n: l10n
|
||||
}));
|
||||
}
|
||||
|
||||
function deleteCache(aId) {
|
||||
if (!CACHE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
localStorage.removeItem(aId);
|
||||
}
|
||||
|
||||
function getCachedContacts(aNodeList) {
|
||||
if (!CACHE_ENABLED || !aNodeList) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_cachedContacts) {
|
||||
_cachedContacts = new Map();
|
||||
}
|
||||
|
||||
if (!_cachedFavorites) {
|
||||
_cachedFavorites = new Map();
|
||||
}
|
||||
|
||||
for (var i = 0; i < aNodeList.length; i++) {
|
||||
var cacheEntry = aNodeList[i];
|
||||
if (cacheEntry.parentNode.id == 'contact-list-ice') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cacheEntry.parentNode.id == 'contacts-list-favorites') {
|
||||
_cachedFavorites.set(cacheEntry.dataset.uuid,
|
||||
cacheEntry.innerHTML);
|
||||
continue;
|
||||
}
|
||||
|
||||
_cachedContacts.set(cacheEntry.dataset.uuid,
|
||||
cacheEntry.innerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
function appendNodesToContainer(aContainer, aNodeList) {
|
||||
if (!CACHE_ENABLED || !aNodeList.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (!_cachedHeaders) {
|
||||
_cachedHeaders = {};
|
||||
}
|
||||
var resolved = false;
|
||||
var fragment = document.createDocumentFragment();
|
||||
aNodeList.forEach((node) => {
|
||||
if (!node.elementName) {
|
||||
return;
|
||||
}
|
||||
var child = document.createElement(node.elementName);
|
||||
node.attributes && node.attributes.forEach((attribute) => {
|
||||
if (!attribute.name || !attribute.value) {
|
||||
return;
|
||||
}
|
||||
child.setAttribute(attribute.name, attribute.value);
|
||||
});
|
||||
if (node.innerHTML) {
|
||||
child.innerHTML = node.innerHTML;
|
||||
}
|
||||
fragment.appendChild(child);
|
||||
|
||||
var headerName = child.id.split('section-group-')[1];
|
||||
_cachedHeaders[headerName] =
|
||||
fragment.querySelector('ol#contacts-list-' + headerName);
|
||||
|
||||
if (!resolved) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
getCachedContacts(fragment.querySelectorAll('li[data-cache=true]'));
|
||||
aContainer.appendChild(fragment);
|
||||
});
|
||||
}
|
||||
|
||||
function removeCacheFromContainer(aContainer) {
|
||||
if (!CACHE_ENABLED || !_cachedContacts) {
|
||||
return;
|
||||
}
|
||||
for (var id of _cachedContacts.keys()) {
|
||||
var items = aContainer.querySelectorAll('li[data-uuid=\"' + id + '\"]');
|
||||
Array.prototype.forEach.call(items, (item) => {
|
||||
var ol = item.parentNode;
|
||||
ol.removeChild(item);
|
||||
if (ol.children.length < 1) {
|
||||
var header = _cachedHeaders[ol.dataset.group];
|
||||
if (header) {
|
||||
var groupTitle = header.parentNode.children[0];
|
||||
groupTitle.classList.add('hide');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function undoCache(aCacheId) {
|
||||
if (!CACHE_ENABLED || !_caches.has(aCacheId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var _cache = _caches.get(aCacheId);
|
||||
|
||||
if (!_cache.containerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
var container = document.getElementById(_cache.containerId);
|
||||
if (!container) {
|
||||
console.warning('Could not remove cached content from ' +
|
||||
_cache.containerId);
|
||||
return;
|
||||
}
|
||||
|
||||
removeCacheFromContainer(container);
|
||||
}
|
||||
|
||||
var Cache = {
|
||||
get active() {
|
||||
return CACHE_ENABLED && _caches.get(FIRST_CHUNK).active;
|
||||
},
|
||||
|
||||
get enabled() {
|
||||
return CACHE_ENABLED;
|
||||
},
|
||||
|
||||
get contacts() {
|
||||
return (CACHE_ENABLED && _cachedContacts) ? _cachedContacts.keys()
|
||||
: null;
|
||||
},
|
||||
|
||||
get favorites() {
|
||||
return (CACHE_ENABLED && _cachedFavorites) ? _cachedFavorites.keys()
|
||||
: null;
|
||||
},
|
||||
|
||||
get length() {
|
||||
return (CACHE_ENABLED && _cachedContacts) ?
|
||||
_cachedContacts.size + _cachedFavorites.size : 0;
|
||||
},
|
||||
|
||||
get headers() {
|
||||
return (CACHE_ENABLED && _cachedHeaders) ? _cachedHeaders
|
||||
: {};
|
||||
},
|
||||
|
||||
// We need to know the order string of the last contact in the cache
|
||||
// so we can insert externaly added contacts in the appropriate order.
|
||||
get lastOrderString() {
|
||||
return (CACHE_ENABLED && _caches.get(FIRST_CHUNK).lastOrderString);
|
||||
},
|
||||
|
||||
get updated() {
|
||||
return (CACHE_ENABLED && _caches.get(FIRST_CHUNK).updated);
|
||||
},
|
||||
|
||||
// For testing purposes only.
|
||||
get _rawContent() {
|
||||
return _caches.get(FIRST_CHUNK).content;
|
||||
},
|
||||
|
||||
set firstChunk(aFirstChunk) {
|
||||
if (!CACHE_ENABLED || !aFirstChunk ||
|
||||
!aFirstChunk.cache || !aFirstChunk.lastOrderString) {
|
||||
return;
|
||||
}
|
||||
setCache({
|
||||
id: FIRST_CHUNK,
|
||||
content: aFirstChunk.cache,
|
||||
lastOrderString: aFirstChunk.lastOrderString
|
||||
});
|
||||
},
|
||||
|
||||
set oneviction(aCallback) {
|
||||
if (!CACHE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
addObserver('oneviction', aCallback);
|
||||
},
|
||||
|
||||
hasContact: function(aUuid) {
|
||||
if (!CACHE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
return _cachedContacts && _cachedContacts.has(aUuid);
|
||||
},
|
||||
|
||||
hasFavorite: function(aUuid) {
|
||||
if (!CACHE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
return _cachedFavorites && _cachedFavorites.has(aUuid);
|
||||
},
|
||||
|
||||
getContact: function(aUuid) {
|
||||
if (!CACHE_ENABLED || !this.hasContact(aUuid)) {
|
||||
return;
|
||||
}
|
||||
// We should get each contact once while rendering the contacts list
|
||||
// to see if what we have in the cache is different to what we have in
|
||||
// the contacts source (most likely mozContacts). Removing the contact
|
||||
// entry from the map allow us to easily check if we have any contact in
|
||||
// the cache that was deleted from the original source and so it needs
|
||||
// to be removed from the view.
|
||||
var contact = _cachedContacts.get(aUuid);
|
||||
_cachedContacts.delete(aUuid);
|
||||
return contact;
|
||||
},
|
||||
|
||||
getFavorite: function(aUuid) {
|
||||
if (!CACHE_ENABLED || !this.hasFavorite(aUuid)) {
|
||||
return;
|
||||
}
|
||||
var contact = _cachedFavorites.get(aUuid);
|
||||
_cachedFavorites.delete(aUuid);
|
||||
return contact;
|
||||
},
|
||||
|
||||
removeContact: function(aUuid) {
|
||||
if (!CACHE_ENABLED ||
|
||||
!this.hasContact(aUuid) ||
|
||||
!this.hasFavorite(aUuid)) {
|
||||
return;
|
||||
}
|
||||
if (_cachedContacts.has(aUuid)) {
|
||||
_cachedContacts.delete(aUuid);
|
||||
}
|
||||
if (_cachedFavorites.has(aUuid)) {
|
||||
_cachedFavorites.delete(aUuid);
|
||||
}
|
||||
},
|
||||
|
||||
_doEvict: function(aUndo) {
|
||||
_cacheEvictionTimer = null;
|
||||
if (aUndo) {
|
||||
undoCache(FIRST_CHUNK);
|
||||
}
|
||||
deleteCache(FIRST_CHUNK);
|
||||
this.cleanup();
|
||||
notifyObservers('oneviction');
|
||||
},
|
||||
|
||||
evict: function(aUndo, aInstant) {
|
||||
if (!CACHE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// There are cases where we need to evict the cache
|
||||
// inmediately, like in web activities, where we can be closed
|
||||
// before doing the eviction.
|
||||
if (aInstant) {
|
||||
this._doEvict(aUndo);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evicting the cache means accessing localStorage.
|
||||
// Use the following logic to evict the cache in a moderate pattern.
|
||||
// 1. When a request to evict the cache is received, start a timer.
|
||||
// 2. If another request is received while the timer is
|
||||
// running, reset it.
|
||||
// 3. Once the timer fires, evict the cache.
|
||||
|
||||
if (_cacheEvictionTimer) {
|
||||
clearTimeout(_cacheEvictionTimer);
|
||||
}
|
||||
|
||||
_cacheEvictionTimer = setTimeout(() => {
|
||||
this._doEvict(aUndo);
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
maybeEvict: function() {
|
||||
if (!CACHE_ENABLED || !this.active) {
|
||||
return;
|
||||
}
|
||||
if ((_caches.get(FIRST_CHUNK).languageDirection !=
|
||||
navigator.mozL10n.language.direction) ||
|
||||
(_caches.get(FIRST_CHUNK).languageCode !=
|
||||
navigator.mozL10n.language.code)) {
|
||||
// If the language direction or code changed, we need to evict and also
|
||||
// undo the applied cache.
|
||||
this.evict(true /* remove applied cache from DOM */);
|
||||
}
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
if (!CACHE_ENABLED) {
|
||||
return;
|
||||
}
|
||||
_caches.get(FIRST_CHUNK).active = false;
|
||||
_caches.get(FIRST_CHUNK).content = null;
|
||||
_cachedContacts = null;
|
||||
_cachedFavorites = null;
|
||||
_cachedHeaders = null;
|
||||
},
|
||||
|
||||
apply: function(aCacheId) {
|
||||
|
||||
if (!CACHE_ENABLED || !_caches.has(aCacheId)) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
var _cache = _caches.get(aCacheId);
|
||||
|
||||
var cache = localStorage.getItem(aCacheId);
|
||||
if (!cache) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
try {
|
||||
cache = JSON.parse(cache);
|
||||
_cache.content = cache.content;
|
||||
_cache.languageDirection = cache.languageDirection;
|
||||
_cache.languageCode = cache.languageCode;
|
||||
_cache.lastOrderString = cache.lastOrderString;
|
||||
_cache.updated = new Date(cache.updated);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if (!_cache.containerId) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
if (cache.l10n) {
|
||||
Object.keys(cache.l10n).forEach(key => {
|
||||
if (!l10nStrings[key] || cache.l10n[key] === 'undefined') {
|
||||
return;
|
||||
}
|
||||
var string = l10nStrings[key];
|
||||
var selector = document.querySelector(string.query);
|
||||
if (!selector) {
|
||||
return;
|
||||
}
|
||||
selector[string.container] = cache.l10n[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (cache.languageDirection) {
|
||||
var html = document.querySelector('html');
|
||||
html.setAttribute('dir', cache.languageDirection);
|
||||
}
|
||||
|
||||
var container = document.getElementById(_cache.containerId);
|
||||
if (!container) {
|
||||
console.warning('Could not apply cached content to ' +
|
||||
_cache.containerId);
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return appendNodesToContainer(container, _cache.content).then(() => {
|
||||
_cache.content = null;
|
||||
_cache.active = true;
|
||||
_caches.set(aCacheId, _cache);
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.Cache = Cache;
|
||||
})();
|
||||
708
gaia/apps/communications/contacts/js/contacts.js
Executable file
708
gaia/apps/communications/contacts/js/contacts.js
Executable file
|
|
@ -0,0 +1,708 @@
|
|||
'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);
|
||||
if (window.navigator.mozSetMessageHandler && window.self == window.top) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
})();
|
||||
108
gaia/apps/communications/contacts/js/contacts_bulk_delete.js
Executable file
108
gaia/apps/communications/contacts/js/contacts_bulk_delete.js
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
/* globals Contacts, utils, contactsRemover, Promise,
|
||||
Search, ConfirmDialog, Loader */
|
||||
'use strict';
|
||||
|
||||
var contacts = window.contacts || {};
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var cancelled = false;
|
||||
|
||||
/**
|
||||
* Loads the overlay class before showing
|
||||
*/
|
||||
function requireOverlay(callback) {
|
||||
Loader.utility('Overlay', callback);
|
||||
}
|
||||
|
||||
// Shows a dialog to confirm the bulk delete
|
||||
var showConfirm = function showConfirm(n) {
|
||||
return new Promise(function doShowConfirm(resolve, reject) {
|
||||
var cancelObject = {
|
||||
title: 'cancel',
|
||||
callback: function onCancel() {
|
||||
ConfirmDialog.hide();
|
||||
reject();
|
||||
}
|
||||
};
|
||||
|
||||
var removeObject = {
|
||||
title: 'delete',
|
||||
isDanger: true,
|
||||
callback: function onRemove() {
|
||||
ConfirmDialog.hide();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
ConfirmDialog.show(null,
|
||||
{'id': 'ContactConfirmDel', 'args': {n: n}}, cancelObject,
|
||||
removeObject);
|
||||
});
|
||||
};
|
||||
|
||||
var doDelete = function doDelete(ids, done) {
|
||||
cancelled = false;
|
||||
var progress = utils.overlay.show('DeletingContacts', 'progressBar');
|
||||
progress.setTotal(ids.length);
|
||||
utils.overlay.showMenu();
|
||||
|
||||
utils.overlay.oncancel = function() {
|
||||
cancelled = true;
|
||||
contactsRemoverObj.finish();
|
||||
};
|
||||
|
||||
var contactsRemoverObj = new contactsRemover();
|
||||
contactsRemoverObj.init(ids, function onInitDone() {
|
||||
contactsRemoverObj.start();
|
||||
});
|
||||
|
||||
contactsRemoverObj.onDeleted = function onDeleted(currentId) {
|
||||
if (window.Search && Search.isInSearchMode()) {
|
||||
Search.invalidateCache();
|
||||
Search.removeContact(currentId);
|
||||
}
|
||||
contacts.List.remove(currentId);
|
||||
progress.update();
|
||||
};
|
||||
|
||||
contactsRemoverObj.onError = function onError() {
|
||||
Contacts.hideOverlay();
|
||||
utils.status.show({
|
||||
id: 'deleteError-general'
|
||||
});
|
||||
contacts.Settings.refresh();
|
||||
};
|
||||
|
||||
contactsRemoverObj.onFinished = function onFinished() {
|
||||
Contacts.hideOverlay();
|
||||
utils.status.show({
|
||||
id: 'DeletedTxt',
|
||||
args: {n: contactsRemoverObj.getDeletedCount()}
|
||||
});
|
||||
contacts.Settings.refresh();
|
||||
|
||||
if (typeof done === 'function') {
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
contactsRemoverObj.onCancelled = contactsRemoverObj.onFinished;
|
||||
|
||||
};
|
||||
// Start the delete of the contacts
|
||||
var performDelete = function performDelete(promise, done) {
|
||||
var self = this;
|
||||
requireOverlay(function onOverlay() {
|
||||
promise.onsuccess = function onSuccess(ids) {
|
||||
showConfirm(ids.length).then(self.doDelete.bind(null, ids, done));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
exports.BulkDelete = {
|
||||
'performDelete': performDelete,
|
||||
'doDelete': doDelete
|
||||
};
|
||||
|
||||
})(window);
|
||||
106
gaia/apps/communications/contacts/js/contacts_remover.js
Executable file
106
gaia/apps/communications/contacts/js/contacts_remover.js
Executable file
|
|
@ -0,0 +1,106 @@
|
|||
/* globals mozContact, ContactsService */
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* Contacts Remover delete the contacts present in phone.
|
||||
* - start: To initiate the deleting process.
|
||||
* - finish: To cancel the deleting process.
|
||||
* - onDeleted: A Contact has been deleted
|
||||
* - onFinished: All Selected contacts have been deleted;
|
||||
* - onError: Error while deleting contacts.
|
||||
*/
|
||||
|
||||
function contactsRemover() {
|
||||
var totalRemoved = 0;
|
||||
var totalSelected = 0;
|
||||
var cancelled = false;
|
||||
/*jshint validthis:true */
|
||||
var self = this;
|
||||
var ids;
|
||||
|
||||
this.init = function(cIds, cb) {
|
||||
if (cIds === null || cIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
ids = cIds;
|
||||
|
||||
if (cb && typeof cb === 'function') { cb(); }
|
||||
};
|
||||
|
||||
this.start = function() {
|
||||
totalRemoved = 0;
|
||||
cancelled = false;
|
||||
totalSelected = ids.length;
|
||||
continueCb(ids);
|
||||
};
|
||||
|
||||
this.finish = function() {
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
this.getDeletedCount = function() {
|
||||
return totalRemoved;
|
||||
};
|
||||
|
||||
function continueCb(ids) {
|
||||
var currentId = ids.shift();
|
||||
if (!currentId || cancelled) {
|
||||
if (totalRemoved === totalSelected) {
|
||||
notifyFinished();
|
||||
}
|
||||
else {
|
||||
notifyCancelled();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
deleteContact(
|
||||
currentId,
|
||||
function(e) {
|
||||
if (e) {
|
||||
notifyError();
|
||||
return;
|
||||
}
|
||||
totalRemoved++;
|
||||
notifyDeleted(currentId);
|
||||
continueCb(ids);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function notifyError() {
|
||||
if (typeof self.onError === 'function') {
|
||||
self.onError();
|
||||
}
|
||||
}
|
||||
|
||||
function notifyCancelled() {
|
||||
if (typeof self.onCancelled === 'function') {
|
||||
window.setTimeout(self.onCancelled, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function notifyFinished() {
|
||||
if (typeof self.onFinished === 'function') {
|
||||
window.setTimeout(self.onFinished, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function notifyDeleted(currentId) {
|
||||
if (typeof self.onDeleted === 'function') {
|
||||
self.onDeleted(currentId);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteContact(currentID, callback) {
|
||||
var contact = new mozContact();
|
||||
contact.id = currentID;
|
||||
|
||||
ContactsService.remove(
|
||||
contact,
|
||||
callback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
window.contactsRemover = contactsRemover;
|
||||
163
gaia/apps/communications/contacts/js/contacts_tag.js
Executable file
163
gaia/apps/communications/contacts/js/contacts_tag.js
Executable file
|
|
@ -0,0 +1,163 @@
|
|||
'use strict';
|
||||
/* global utils */
|
||||
/* exported ContactsTag */
|
||||
|
||||
var ContactsTag = (function() {
|
||||
var originalTag = null;
|
||||
var selectedTag = null;
|
||||
var customTag = null;
|
||||
|
||||
var setCustomTag = function setCustomTag(element) {
|
||||
customTag = element;
|
||||
};
|
||||
|
||||
var setCustomTagVisibility = function setCustomTagVisibility(value) {
|
||||
if (!customTag) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
customTag.classList.remove('hide');
|
||||
}
|
||||
else {
|
||||
customTag.classList.add('hide');
|
||||
}
|
||||
};
|
||||
|
||||
var unMarkTag = function unMarkTag(tag) {
|
||||
if (tag) {
|
||||
tag.classList.remove('icon');
|
||||
tag.classList.remove('icon-selected');
|
||||
|
||||
tag.removeAttribute('aria-selected');
|
||||
}
|
||||
};
|
||||
|
||||
var markTag = function markTag(tag) {
|
||||
if (tag) {
|
||||
tag.classList.add('icon');
|
||||
tag.classList.add('icon-selected');
|
||||
|
||||
tag.setAttribute('aria-selected', true);
|
||||
}
|
||||
};
|
||||
|
||||
var touchCustomTag = function touchCustomTag(callback) {
|
||||
unMarkTag(selectedTag);
|
||||
|
||||
selectedTag = null;
|
||||
|
||||
if (callback !== undefined && typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
var fillTagOptions = function fillTagOptions(target, _originalTag, options) {
|
||||
utils.dom.removeChildNodes(target);
|
||||
originalTag = _originalTag;
|
||||
|
||||
var selectedLink;
|
||||
/* jshint loopfunc:true */
|
||||
for (var option in options) {
|
||||
var tagLink = document.createElement('button');
|
||||
tagLink.dataset.index = option;
|
||||
tagLink.setAttribute('data-l10n-id', options[option].type);
|
||||
tagLink.setAttribute('data-value', options[option].type);
|
||||
tagLink.setAttribute('role', 'option');
|
||||
tagLink.classList.add('tagItem');
|
||||
|
||||
tagLink.addEventListener('click', function(event) {
|
||||
var tag = event.target;
|
||||
selectTag(tag);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
if (originalTag.dataset.value == options[option].type) {
|
||||
selectedLink = tagLink;
|
||||
}
|
||||
|
||||
var tagItem = document.createElement('li');
|
||||
tagItem.setAttribute('role', 'presentation');
|
||||
tagItem.appendChild(tagLink);
|
||||
target.appendChild(tagItem);
|
||||
}
|
||||
|
||||
customTag.value = '';
|
||||
if (!selectedLink && originalTag.textContent) {
|
||||
customTag.value = originalTag.textContent;
|
||||
}
|
||||
selectTag(selectedLink);
|
||||
};
|
||||
|
||||
var selectTag = function selectTag(tag) {
|
||||
if (tag == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Clean any trace of the custom tag
|
||||
customTag.value = '';
|
||||
|
||||
unMarkTag(selectedTag);
|
||||
|
||||
markTag(tag);
|
||||
|
||||
selectedTag = tag;
|
||||
};
|
||||
|
||||
var clickDone = function clickDone(callback) {
|
||||
if (selectedTag) {
|
||||
originalTag.textContent = selectedTag.textContent;
|
||||
originalTag.dataset.l10nId = selectedTag.dataset.l10nId;
|
||||
originalTag.dataset.value = selectedTag.dataset.value;
|
||||
} else if (customTag.value.length > 0) {
|
||||
originalTag.textContent = customTag.value;
|
||||
originalTag.dataset.value = customTag.value;
|
||||
}
|
||||
originalTag = null;
|
||||
|
||||
if (callback !== undefined && typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
// Filter tags to be shown when selecting an item type (work, birthday, etc)
|
||||
// This is particularly useful for dates as we cannot have multiple instances
|
||||
// of them (only one birthday, only one anniversary)
|
||||
function filterTags(type, currentNode, tags) {
|
||||
var element = document.querySelector(
|
||||
'[data-template]' + '.' + type + '-' + 'template');
|
||||
if (!element || !element.dataset.exclusive) {
|
||||
return tags;
|
||||
}
|
||||
|
||||
// If the type is exclusive the tag options are filtered according to
|
||||
// the existing ones
|
||||
var newOptions = tags.slice(0);
|
||||
|
||||
var sameType = document.querySelectorAll('.' + type + '-template');
|
||||
if (sameType.length > 1) {
|
||||
/* jshint loopfunc:true */
|
||||
for (var j = 0; j < sameType.length; j++) {
|
||||
var itemSame = sameType.item(j);
|
||||
var tagNode = itemSame.querySelector('[data-field="type"]');
|
||||
if (tagNode !== currentNode &&
|
||||
!itemSame.classList.contains('facebook')) {
|
||||
newOptions = newOptions.filter(function(ele) {
|
||||
return ele.type != tagNode.dataset.value;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
return {
|
||||
'setCustomTag': setCustomTag,
|
||||
'touchCustomTag': touchCustomTag,
|
||||
'fillTagOptions': fillTagOptions,
|
||||
'clickDone': clickDone,
|
||||
'setCustomTagVisibility': setCustomTagVisibility,
|
||||
'filterTags': filterTags
|
||||
};
|
||||
})();
|
||||
119
gaia/apps/communications/contacts/js/deferred_actions.js
Executable file
119
gaia/apps/communications/contacts/js/deferred_actions.js
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
'use strict';
|
||||
|
||||
/* global fb */
|
||||
/* global fbLoader */
|
||||
/* global ImportStatusData */
|
||||
/* global LazyLoader */
|
||||
/* global Migrator */
|
||||
/* global utils */
|
||||
|
||||
/* exported DeferredActions */
|
||||
|
||||
// Methods not related with the list rendering that must be executed after
|
||||
// render to not damage performance.
|
||||
var DeferredActions = (function() {
|
||||
var config;
|
||||
|
||||
var TOTAL_RETRIES = 4;
|
||||
|
||||
var execute = function execute() {
|
||||
config = utils.cookie.load();
|
||||
|
||||
if (!fbLoader.loaded) {
|
||||
window.addEventListener('facebookLoaded', doExecute);
|
||||
return;
|
||||
}
|
||||
doExecute();
|
||||
};
|
||||
|
||||
var doExecute = function doExecute() {
|
||||
window.removeEventListener('facebookLoaded', doExecute);
|
||||
|
||||
checkFacebookSynchronization(config);
|
||||
checkVersionMigration(config);
|
||||
checkFacebookSanity(config);
|
||||
};
|
||||
|
||||
// If FB Cleaning data was ongoing we finish it
|
||||
// This avoids keeping the contacts database inconsistent. See bug 1073684
|
||||
function checkFacebookSanity(config) {
|
||||
if (!config || !config.fbCleaningInProgress ||
|
||||
config.fbCleaningInProgress > TOTAL_RETRIES) {
|
||||
return;
|
||||
}
|
||||
|
||||
var req = fb.utils.clearFbData();
|
||||
req.onsuccess = function() {
|
||||
var cleaner = req.result;
|
||||
|
||||
cleaner.onsuccess = function() {
|
||||
console.log('Finished clean Fb data');
|
||||
fb.markFbCleaningInProgress(0);
|
||||
};
|
||||
cleaner.onerror = function() {
|
||||
console.error('FB Cleaner reported an error: ',
|
||||
req.error && req.error.name);
|
||||
fb.markFbCleaningInProgress(++config.fbCleaningInProgress);
|
||||
};
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
console.error('Unable to start cleaning', req.error && req.error.name);
|
||||
fb.markFbCleaningInProgress(++config.fbCleaningInProgress);
|
||||
};
|
||||
}
|
||||
|
||||
function checkFacebookSynchronization(config) {
|
||||
// Checks if we should set a fb sync alarm when fb sync has been done in ftu
|
||||
if (config && config.fbScheduleDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
LazyLoader.load([
|
||||
'/facebook/js/fb_sync.js',
|
||||
'/shared/js/contacts/import/import_status_data.js'
|
||||
], performFbSync);
|
||||
}
|
||||
|
||||
function performFbSync() {
|
||||
var fbutils = fb.utils;
|
||||
|
||||
function neverExecuteAgain() {
|
||||
ImportStatusData.remove(fbutils.SCHEDULE_SYNC_KEY);
|
||||
utils.cookie.update({fbScheduleDone: true});
|
||||
navigator.removeIdleObserver(idleObserver);
|
||||
idleObserver = null;
|
||||
}
|
||||
|
||||
var idleObserver = {
|
||||
time: 3,
|
||||
onidle: function onidle() {
|
||||
ImportStatusData.get(fbutils.SCHEDULE_SYNC_KEY).then(function(date) {
|
||||
if (date) {
|
||||
fbutils.setLastUpdate(date, function() {
|
||||
var req = fb.sync.scheduleNextSync();
|
||||
req.onsuccess = neverExecuteAgain;
|
||||
req.onerror = neverExecuteAgain;
|
||||
});
|
||||
} else {
|
||||
neverExecuteAgain();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
navigator.addIdleObserver(idleObserver);
|
||||
}
|
||||
|
||||
function checkVersionMigration(config) {
|
||||
if (!config || !config.fbMigrated || !config.accessTokenMigrated) {
|
||||
LazyLoader.load('js/migrator.js', function() {
|
||||
Migrator.start(config);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'execute': execute
|
||||
};
|
||||
})();
|
||||
200
gaia/apps/communications/contacts/js/export/bt.js
Executable file
200
gaia/apps/communications/contacts/js/export/bt.js
Executable file
|
|
@ -0,0 +1,200 @@
|
|||
/* global getStorageIfAvailable, getUnusedFilename, ContactToVcardBlob,
|
||||
MozActivity, utils */
|
||||
|
||||
/* exported ContactsBTExport */
|
||||
'use strict';
|
||||
|
||||
var ContactsBTExport = function ContactsBTExport() {
|
||||
var contacts;
|
||||
var progressStep;
|
||||
var cancelled = false;
|
||||
var _ = navigator.mozL10n.get;
|
||||
|
||||
var _setContactsToExport = function btex_setContactsToExport(cts) {
|
||||
contacts = cts;
|
||||
};
|
||||
|
||||
var _hasDeterminativeProgress = function btex_hasDeterminativeProgress() {
|
||||
return false;
|
||||
};
|
||||
|
||||
var _getExportTitle = function btex_getExportTitle() {
|
||||
return 'btExport-title';
|
||||
};
|
||||
|
||||
var _setProgressStep = function btex_setProgressStep(p) {
|
||||
progressStep = p;
|
||||
};
|
||||
|
||||
var _getGivenName = function _getGivenName(contact) {
|
||||
return (Array.isArray(contact.givenName) && contact.givenName[0] &&
|
||||
contact.givenName[0].trim());
|
||||
};
|
||||
var _getLastName = function _getLastName(contact) {
|
||||
return (Array.isArray(contact.familyName) && contact.familyName[0] &&
|
||||
contact.familyName[0].trim());
|
||||
};
|
||||
var _getFileName = function _getFileName() {
|
||||
var filename = [];
|
||||
if (contacts && contacts.length === 1) {
|
||||
var contact = contacts[0],
|
||||
givenName = _getGivenName(contact),
|
||||
lastName = _getLastName(contact);
|
||||
if (givenName) {
|
||||
filename.push(givenName);
|
||||
}
|
||||
if (lastName) {
|
||||
filename.push(lastName);
|
||||
}
|
||||
if (filename.length === 0) {
|
||||
if (contact.org && contact.org.length > 0) {
|
||||
filename.push(contact.org[0]);
|
||||
} else if (contact.tel && contact.tel.length > 0) {
|
||||
filename.push(contact.tel[0].value);
|
||||
} else if (contact.email && contact.email.length > 0) {
|
||||
filename.push(contact.email[0].value);
|
||||
} else {
|
||||
filename.push(_('noName'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var today = new Date();
|
||||
filename.push(
|
||||
today.getFullYear(),
|
||||
today.getMonth() + 1,
|
||||
today.getDate(),
|
||||
contacts.length
|
||||
);
|
||||
}
|
||||
|
||||
return filename.join('_')
|
||||
.replace(/[^a-z0-9]/gi, '_')
|
||||
.toLowerCase() +
|
||||
'.vcf';
|
||||
};
|
||||
var _getStorage = function _getStorage(fileName, blob, callback) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new Error('getStorage requires a callback function');
|
||||
}
|
||||
getStorageIfAvailable('sdcard', blob.size,
|
||||
function onSuccess(storage) {
|
||||
// Get the final version of the proposed file name
|
||||
getUnusedFilename(storage, fileName, function onfilename(name) {
|
||||
callback(null, storage, name);
|
||||
});
|
||||
},
|
||||
callback //onerror
|
||||
);
|
||||
};
|
||||
var _saveToSdcard = function _saveToSdcard(storage, name, blob, callback) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new Error('saveToSDcard requires a callback function');
|
||||
}
|
||||
|
||||
var request = storage.addNamed(blob, name);
|
||||
request.onsuccess = function(evt) {
|
||||
callback(null, evt.target.result); // returns the full filepath
|
||||
};
|
||||
request.onerror = callback;
|
||||
};
|
||||
var _getFile = function _getFile(storage, filename, callback) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new Error('getFile requires a callback function');
|
||||
}
|
||||
var request = storage.get(filename);
|
||||
request.onsuccess = function() {
|
||||
callback(null, request.result); // returns a File object
|
||||
};
|
||||
request.onerror = callback;
|
||||
};
|
||||
|
||||
var cancelExport = function cancelExport() {
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
var _doExport = function btex_doExport(finishCallback) {
|
||||
if (typeof finishCallback !== 'function') {
|
||||
throw new Error('BT export requires a callback function');
|
||||
}
|
||||
|
||||
var checkError = function checkError(error) {
|
||||
if (error === null) {
|
||||
return false;
|
||||
}
|
||||
var reason = error;
|
||||
// numeric error means not enough space available
|
||||
if (parseInt(error, 10) >= 0) {
|
||||
reason = 'noSpace';
|
||||
}
|
||||
finishCallback({ 'reason': reason }, 0, false);
|
||||
return true;
|
||||
};
|
||||
|
||||
ContactToVcardBlob(contacts, function onContacts(blob) {
|
||||
if (cancelled) {
|
||||
finishCallback(null, 0, false);
|
||||
return;
|
||||
}
|
||||
_getStorage(_getFileName(), blob,
|
||||
function onStorage(error, storage, filename) {
|
||||
if (checkError(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cancelled) {
|
||||
finishCallback(null, 0, false);
|
||||
return;
|
||||
}
|
||||
|
||||
utils.overlay.hideMenu();
|
||||
_saveToSdcard(storage, filename, blob,
|
||||
function onVcardSaved(error, filepath) {
|
||||
if (checkError(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_getFile(storage, filepath, function onFileRetrieved(error, file) {
|
||||
if (checkError(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var a = new MozActivity({
|
||||
name: 'share-via-bluetooth-only',
|
||||
data: {
|
||||
type: 'text/vcard',
|
||||
number: 1,
|
||||
blobs: [file],
|
||||
filenames: [filename],
|
||||
filepaths: [filepath]
|
||||
}
|
||||
});
|
||||
|
||||
a.onsuccess = function() {
|
||||
finishCallback(null, contacts.length);
|
||||
};
|
||||
|
||||
a.onerror = function(e) {
|
||||
if (a.error.name === 'NO_PROVIDER') {
|
||||
alert(_('share-noprovider'));
|
||||
} else {
|
||||
console.warn('share activity error:', a.error.name);
|
||||
}
|
||||
finishCallback({'reason': a.error}, 0, a.error.name);
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
'setContactsToExport': _setContactsToExport,
|
||||
'shouldShowProgress': function btex_shouldShowProgress() { return true; },
|
||||
'hasDeterminativeProgress': _hasDeterminativeProgress,
|
||||
'getExportTitle': _getExportTitle,
|
||||
'setProgressStep': _setProgressStep,
|
||||
'doExport': _doExport,
|
||||
'cancelExport' : cancelExport,
|
||||
get name() { return 'BT';} // handling error messages on contacts_exporter
|
||||
};
|
||||
};
|
||||
197
gaia/apps/communications/contacts/js/export/contacts_exporter.js
Executable file
197
gaia/apps/communications/contacts/js/export/contacts_exporter.js
Executable file
|
|
@ -0,0 +1,197 @@
|
|||
/* global utils, ConfirmDialog, Contacts, ContactsService, Loader */
|
||||
'use strict';
|
||||
|
||||
//
|
||||
// Generic module to do contacts export.
|
||||
//
|
||||
// Once passed to this module an array of mozContacts objects
|
||||
// and setup the strategy (mechanism to export), will perform
|
||||
// the common steps taken place during the process of exporting
|
||||
// the information.
|
||||
|
||||
// Those steps are:
|
||||
|
||||
// - Preparation: (optional), may launch extra UI to setup some configuration
|
||||
// and transform the mozcontacts object to any extra format
|
||||
// - Export: Here is where the real import happens, could provide three known
|
||||
// scenarios:
|
||||
// - External handling of the export (web activity, ect.)
|
||||
// - Progress of the import:
|
||||
// - Defined progress activity
|
||||
// - Undefined progress
|
||||
// - Result of the export
|
||||
|
||||
window.ContactsExporter = function ContactsExporter(theStrategy) {
|
||||
|
||||
var contacts;
|
||||
var strategy = theStrategy;
|
||||
var hasProgress = false;
|
||||
var determinativeProgress = false;
|
||||
var progress;
|
||||
|
||||
// XXX: Bug 904623 since we cannot fetch a list of contacts by
|
||||
// contact id, we will need to fetch them all and filter
|
||||
var _init = function _init(theContacts, cb) {
|
||||
if (theContacts === null || theContacts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ContactsService.getAll(function(e, contactsFromAPI) {
|
||||
if (e) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
contacts = [];
|
||||
contactsFromAPI.forEach(function onContact(ct) {
|
||||
if (theContacts.indexOf(ct.id) !== -1) {
|
||||
contacts.push(ct);
|
||||
}
|
||||
});
|
||||
if (cb) {
|
||||
cb(contacts);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Checks if the export module has been properly configured
|
||||
// and start the process of exporting.
|
||||
//
|
||||
var _start = function _start() {
|
||||
if (!contacts || !strategy) {
|
||||
throw new Error('Not properly configured');
|
||||
}
|
||||
|
||||
strategy.setContactsToExport(contacts);
|
||||
|
||||
// Check if we have a 'Preparation step'
|
||||
if (typeof strategy.prepare === 'function') {
|
||||
strategy.prepare(_doExport);
|
||||
} else {
|
||||
_doExport();
|
||||
}
|
||||
};
|
||||
|
||||
var _doExport = function _doExport() {
|
||||
if (typeof strategy.shouldShowProgress === 'function' &&
|
||||
strategy.shouldShowProgress()) {
|
||||
hasProgress = true;
|
||||
_configureProgress();
|
||||
_displayProgress();
|
||||
}
|
||||
|
||||
strategy.doExport(_doHandleResult);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback invoked when the exporting process finished.
|
||||
*
|
||||
* @param {Object} error Not null in case an error happened
|
||||
* @param {Number} exported Number of contacts successfuly exported
|
||||
* @param {Boolean} isRecoverable In case of error, whether it is recoverable
|
||||
*/
|
||||
var _doHandleResult = function _doHandleResult(error, exported,
|
||||
isRecoverable) {
|
||||
if (hasProgress && !error) {
|
||||
utils.overlay.hide();
|
||||
}
|
||||
|
||||
if (error) {
|
||||
// If it was cancelled by the user we don't show the error screen.
|
||||
if (error.reason && error.reason.name === 'cancelled') {
|
||||
utils.overlay.hide();
|
||||
} else {
|
||||
var cancel = {
|
||||
title: 'cancel',
|
||||
callback: function() {
|
||||
utils.overlay.hide();
|
||||
ConfirmDialog.hide();
|
||||
_showStatus(exported, contacts.length);
|
||||
}
|
||||
};
|
||||
|
||||
var retry = {
|
||||
title: 'retry',
|
||||
isRecommend: true,
|
||||
callback: function() {
|
||||
utils.overlay.hide();
|
||||
ConfirmDialog.hide();
|
||||
// And now the action is reproduced one more time
|
||||
window.setTimeout(_doExport, 0);
|
||||
}
|
||||
};
|
||||
|
||||
if (isRecoverable === false) {
|
||||
retry = null;
|
||||
}
|
||||
|
||||
var errorString = 'exportError-' + strategy.name + '-';
|
||||
ConfirmDialog.show('exportErrorTitle',
|
||||
errorString + error.reason, cancel, retry);
|
||||
|
||||
Contacts.hideOverlay();
|
||||
|
||||
console.error('An error occurred during the export: ',
|
||||
error.reason.name || error.reason);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_showStatus(exported, contacts.length);
|
||||
};
|
||||
|
||||
var _showStatus = function(exported, total) {
|
||||
// TODO: Better mechanism to show result
|
||||
var msgId = {
|
||||
id: 'contactsExported2',
|
||||
args: {
|
||||
'exported': exported,
|
||||
'total': total
|
||||
}
|
||||
};
|
||||
|
||||
utils.status.show(msgId);
|
||||
};
|
||||
|
||||
//
|
||||
// Based on the strategy configure the progress display to show a
|
||||
// determinative or indeterminate ui depending on the strategy
|
||||
//
|
||||
var _configureProgress = function _configureProgress() {
|
||||
determinativeProgress =
|
||||
strategy.hasDeterminativeProgress !== undefined &&
|
||||
strategy.hasDeterminativeProgress();
|
||||
};
|
||||
|
||||
//
|
||||
// Shows the progress dialog based on the
|
||||
//
|
||||
var _displayProgress = function _displayProgress() {
|
||||
var progressClass = determinativeProgress ? 'progressBar' : 'spinner';
|
||||
|
||||
Loader.utility('Overlay', function _loaded() {
|
||||
progress = utils.overlay.show(
|
||||
strategy.getExportTitle(),
|
||||
progressClass,
|
||||
null
|
||||
);
|
||||
utils.overlay.showMenu();
|
||||
utils.overlay.oncancel = function() {
|
||||
strategy.cancelExport();
|
||||
utils.overlay.hide();
|
||||
};
|
||||
|
||||
// Allow the strategy to setup the progress bar
|
||||
if (determinativeProgress) {
|
||||
progress.setTotal(contacts.length);
|
||||
strategy.setProgressStep(progress.update);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
'init': _init,
|
||||
'start': _start
|
||||
};
|
||||
|
||||
};
|
||||
168
gaia/apps/communications/contacts/js/export/sd.js
Executable file
168
gaia/apps/communications/contacts/js/export/sd.js
Executable file
|
|
@ -0,0 +1,168 @@
|
|||
/* globals _, ContactToVcard, getStorageIfAvailable, getUnusedFilename
|
||||
*/
|
||||
|
||||
/* exported ContactsSDExport */
|
||||
|
||||
'use strict';
|
||||
|
||||
var ContactsSDExport = function ContactsSDExport() {
|
||||
|
||||
var contacts;
|
||||
var progressStep;
|
||||
var cancelled = false;
|
||||
|
||||
var setContactsToExport = function setContactsToExport(cts) {
|
||||
contacts = cts;
|
||||
};
|
||||
|
||||
var hasDeterminativeProgress = function hasDeterminativeProgress() {
|
||||
return false;
|
||||
};
|
||||
|
||||
var setProgressStep = function setProgressStep(p) {
|
||||
progressStep = p;
|
||||
};
|
||||
|
||||
var getExportTitle = function getExportTitle() {
|
||||
return 'memoryCardExport-title';
|
||||
};
|
||||
|
||||
var hasGivenName = function hasGivenName(contact) {
|
||||
return (Array.isArray(contact.givenName) && contact.givenName[0] &&
|
||||
contact.givenName[0].trim());
|
||||
};
|
||||
|
||||
var hasFamilyName = function hasFamilyName(contact) {
|
||||
return (Array.isArray(contact.familyName) && contact.familyName[0] &&
|
||||
contact.familyName[0].trim());
|
||||
};
|
||||
|
||||
var getFileName = function getFileName() {
|
||||
var filename = [];
|
||||
if (contacts && contacts.length === 1) {
|
||||
var contact = contacts[0],
|
||||
hasName = false;
|
||||
if (hasGivenName(contact)) {
|
||||
filename.push(contact.givenName[0]);
|
||||
hasName = true;
|
||||
}
|
||||
if (hasFamilyName(contact)) {
|
||||
filename.push(contact.familyName[0]);
|
||||
hasName = true;
|
||||
}
|
||||
if (!hasName) {
|
||||
if (contact.org && contact.org.length > 0) {
|
||||
filename.push(contact.org[0]);
|
||||
} else if (contact.tel && contact.tel.length > 0) {
|
||||
filename.push(contact.tel[0].value);
|
||||
} else if (contact.email && contact.email.length > 0) {
|
||||
filename.push(contact.email[0].value);
|
||||
} else {
|
||||
filename.push(_('noName'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var today = new Date();
|
||||
filename.push(
|
||||
today.getFullYear(),
|
||||
today.getMonth() + 1,
|
||||
today.getDate(),
|
||||
contacts.length
|
||||
);
|
||||
}
|
||||
|
||||
return filename.join('_')
|
||||
.replace(/[^a-z0-9]/gi, '_')
|
||||
.toLowerCase() +
|
||||
'.vcf';
|
||||
};
|
||||
|
||||
var getStorage = function getStorage(fileName, blob, callback) {
|
||||
getStorageIfAvailable('sdcard', blob.size,
|
||||
function onSuccess(storage) {
|
||||
// Get the final version of the proposed file name
|
||||
getUnusedFilename(storage, fileName, function onFinalName(name) {
|
||||
callback(null, storage, name);
|
||||
});
|
||||
},
|
||||
function onError(type) {
|
||||
callback(type);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var cancelExport = function cancelExport() {
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
var doExport = function doExport(finishCallback) {
|
||||
/* XXX: We use a batch-conversion size of 2MiB. This ensures that the
|
||||
* process won't go out of memory even on low-end devices. This should be
|
||||
* revisited once append-to-file functionality becomes available. */
|
||||
var batchSize = 2 * 1024 * 1024;
|
||||
var count = 0;
|
||||
var pendingBatches = 0;
|
||||
var done = false;
|
||||
var cancelButton = document.querySelector('#cancel-overlay');
|
||||
|
||||
if (typeof finishCallback !== 'function') {
|
||||
throw new Error('SD export requires a callback function');
|
||||
}
|
||||
|
||||
ContactToVcard(contacts, function onContacts(vcards, nCards) {
|
||||
if (cancelled) {
|
||||
finishCallback(null, 0);
|
||||
return;
|
||||
}
|
||||
var blob = new Blob([vcards], {'type': 'text/vcard'});
|
||||
|
||||
pendingBatches++;
|
||||
|
||||
getStorage(getFileName(), blob,
|
||||
function onStorage(error, storage, finalName) {
|
||||
if (error !== null) {
|
||||
var reason = error;
|
||||
// numeric error means not enough space available
|
||||
if (parseInt(error, 10) >= 0) {
|
||||
reason = 'noSpace';
|
||||
}
|
||||
finishCallback({
|
||||
'reason': reason
|
||||
}, count, true);
|
||||
return;
|
||||
}
|
||||
if (cancelled) {
|
||||
finishCallback(null, 0);
|
||||
return;
|
||||
}
|
||||
cancelButton.disabled = true;
|
||||
var request = storage.addNamed(blob, finalName);
|
||||
request.onsuccess = function onSuccess() {
|
||||
count += nCards;
|
||||
pendingBatches--;
|
||||
|
||||
if (done && (pendingBatches === 0)) {
|
||||
finishCallback(null, count, false);
|
||||
}
|
||||
};
|
||||
request.onerror = function onError(e) {
|
||||
finishCallback({ 'reason': e }, count, true);
|
||||
};
|
||||
});
|
||||
}, function finish() {
|
||||
done = true;
|
||||
}, batchSize);
|
||||
};
|
||||
|
||||
return {
|
||||
'setContactsToExport': setContactsToExport,
|
||||
'shouldShowProgress': function shouldShowProgress() { return true; },
|
||||
'hasDeterminativeProgress': hasDeterminativeProgress,
|
||||
'getExportTitle': getExportTitle,
|
||||
'doExport': doExport,
|
||||
'setProgressStep': setProgressStep,
|
||||
'cancelExport': cancelExport,
|
||||
get name() { return 'SD'; } // handling error messages on contacts_exporter
|
||||
};
|
||||
|
||||
};
|
||||
195
gaia/apps/communications/contacts/js/export/sim.js
Executable file
195
gaia/apps/communications/contacts/js/export/sim.js
Executable file
|
|
@ -0,0 +1,195 @@
|
|||
/* exported ContactsSIMExport */
|
||||
/* global ContactsService */
|
||||
'use strict';
|
||||
|
||||
var ContactsSIMExport = function ContactsSIMExport(icc) {
|
||||
|
||||
var contacts;
|
||||
var progressStep;
|
||||
var exported;
|
||||
var notExported;
|
||||
var cancelled = false;
|
||||
|
||||
var ERRORS = {
|
||||
'NoFreeRecordFound': false,
|
||||
'CannotAccessPhoneBook': true
|
||||
};
|
||||
|
||||
var setContactsToExport = function setContactsToExport(cts) {
|
||||
contacts = cts;
|
||||
};
|
||||
|
||||
var hasDeterminativeProgress = function hasDeterminativeProgress() {
|
||||
return contacts.length > 1;
|
||||
};
|
||||
|
||||
var setProgressStep = function setProgressStep(p) {
|
||||
progressStep = p;
|
||||
};
|
||||
|
||||
var getExportTitle = function getExportTitle() {
|
||||
return 'simExport-title';
|
||||
};
|
||||
|
||||
var doExport = function doExport(finishCallback) {
|
||||
if (typeof finishCallback !== 'function') {
|
||||
throw new Error('SIM export requires a callback function');
|
||||
}
|
||||
|
||||
exported = [];
|
||||
notExported = [];
|
||||
|
||||
// We should control this state before doing the export
|
||||
// but a second check is healthy
|
||||
if (!icc) {
|
||||
finishCallback({
|
||||
'reason': 'Unavailable'
|
||||
}, 0, false);
|
||||
return;
|
||||
}
|
||||
|
||||
_doExport(0, finishCallback);
|
||||
};
|
||||
|
||||
// Returns the iccContactId to be used for exporting this contact
|
||||
// undefined if not iccContactId is found
|
||||
// url content is urn:uuid:<iccid>-icccontactid
|
||||
var _getIccContactId = function _getIccContactId(theContact) {
|
||||
var out;
|
||||
var contactUrl = theContact.url;
|
||||
if (!Array.isArray(contactUrl)) {
|
||||
return out;
|
||||
}
|
||||
|
||||
for (var j = 0; j < contactUrl.length; j++) {
|
||||
var aUrl = contactUrl[j];
|
||||
if (aUrl.type.indexOf('source') !== -1 &&
|
||||
aUrl.type.indexOf('sim') !== -1) {
|
||||
var value = aUrl.value.split(':')[2];
|
||||
var iccInfo = value.split('-');
|
||||
if (iccInfo[0] === _getIccId()) {
|
||||
out = iccInfo[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
var _generateIccContactUrl = function _generateIccContactUrl(contactid,
|
||||
iccId) {
|
||||
var urlValue = 'urn:' + 'uuid:' + (iccId || 'iccId') + '-' + contactid;
|
||||
return {
|
||||
type: ['source', 'sim'],
|
||||
value: urlValue
|
||||
};
|
||||
};
|
||||
|
||||
var _getIccId = function _getIccId() {
|
||||
return icc.iccInfo && icc.iccInfo.iccid;
|
||||
};
|
||||
|
||||
var _updateContactSimSource = function _updateContactSimSource(theContact,
|
||||
iccContact, cb, errorCb) {
|
||||
if (!iccContact) {
|
||||
window.console.warn('No Icc Contact provided');
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
theContact.url = theContact.url || [];
|
||||
theContact.url.push(_generateIccContactUrl(iccContact.id, _getIccId()));
|
||||
|
||||
ContactsService.save(
|
||||
theContact,
|
||||
function(e) {
|
||||
if (e) {
|
||||
errorCb(e);
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var cancelExport = function cancelExport() {
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
var _doExport = function _doExport(step, finishCallback) {
|
||||
if (step == contacts.length || cancelled) {
|
||||
finishCallback(null, exported.length);
|
||||
return;
|
||||
}
|
||||
|
||||
var next = function next(success, contact) {
|
||||
var resultArray = success ? exported : notExported;
|
||||
resultArray.push(contact);
|
||||
if (progressStep) {
|
||||
progressStep();
|
||||
}
|
||||
step++;
|
||||
_doExport(step, finishCallback);
|
||||
};
|
||||
|
||||
var theContact = contacts[step];
|
||||
var originalContactId = theContact.id;
|
||||
var idToUpdate = _getIccContactId(theContact);
|
||||
if (!idToUpdate) {
|
||||
theContact.id = null;
|
||||
}
|
||||
else {
|
||||
theContact.id = idToUpdate;
|
||||
}
|
||||
|
||||
var request = icc.updateContact('adn', theContact);
|
||||
request.onsuccess = function onsuccess() {
|
||||
// Restoring the original contact.id to avoid duplicating it
|
||||
theContact.id = originalContactId;
|
||||
|
||||
if (idToUpdate) {
|
||||
next(true, theContact);
|
||||
return;
|
||||
}
|
||||
// Now it is needed to save the provided id in case we don't have one
|
||||
var iccContact = request.result;
|
||||
_updateContactSimSource(theContact, iccContact,
|
||||
function export_update_success() {
|
||||
next(true, theContact);
|
||||
}, function export_update_error(err) {
|
||||
window.console.error('Error while updating: ', err.name);
|
||||
next(true, theContact);
|
||||
});
|
||||
};
|
||||
|
||||
request.onerror = function onerror(e) {
|
||||
var error = e.target.error;
|
||||
window.console.error('Error while exporting: ', originalContactId,
|
||||
error.name);
|
||||
|
||||
var errorName = typeof ERRORS[error.name] === 'boolean' ?
|
||||
error.name : 'GenericError';
|
||||
|
||||
if (errorName !== 'GenericError') {
|
||||
finishCallback({
|
||||
'reason': errorName
|
||||
}, exported.length, ERRORS[error.name]);
|
||||
}
|
||||
else {
|
||||
next(false, theContact);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
'setContactsToExport': setContactsToExport,
|
||||
'shouldShowProgress': function() { return true; },
|
||||
'hasDeterminativeProgress': hasDeterminativeProgress,
|
||||
'getExportTitle': getExportTitle,
|
||||
'doExport': doExport,
|
||||
'setProgressStep': setProgressStep,
|
||||
'cancelExport' : cancelExport,
|
||||
get name() { return 'SIM';} // handling error messages on contacts_exporter
|
||||
};
|
||||
|
||||
};
|
||||
314
gaia/apps/communications/contacts/js/fb/datastore_migrator.js
Executable file
314
gaia/apps/communications/contacts/js/fb/datastore_migrator.js
Executable file
|
|
@ -0,0 +1,314 @@
|
|||
'use strict';
|
||||
|
||||
/* global LazyLoader, fb, contacts, utils */
|
||||
/* exported DatastoreMigration */
|
||||
|
||||
var DatastoreMigration = function(db) {
|
||||
var active = false;
|
||||
var ongoingMigration = false;
|
||||
var migrator;
|
||||
var idleObserver;
|
||||
var self = this;
|
||||
|
||||
// Only really needed for testing purposes
|
||||
Object.defineProperty(this, 'db', {
|
||||
set: function(val) {
|
||||
if (migrator) {
|
||||
migrator.db = val;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var IdleObserver = function() {
|
||||
var self = this;
|
||||
|
||||
this._files = [
|
||||
'/shared/js/fb/fb_request.js',
|
||||
'/shared/js/contacts/import/facebook/fb_data.js'
|
||||
];
|
||||
|
||||
// 3 seconds idle time
|
||||
this.time = 3;
|
||||
|
||||
this.onidle = function() {
|
||||
LazyLoader.load(self._files, function() {
|
||||
onidle();
|
||||
});
|
||||
};
|
||||
|
||||
this.onactive = function() {
|
||||
LazyLoader.load(self._files, function() {
|
||||
onactive();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
function DatastoreMigrator(db) {
|
||||
var IDB_NAME = 'Gaia_Facebook_Friends';
|
||||
var objStoreName;
|
||||
|
||||
var SLICE_SIZE = 5;
|
||||
var initialized = false;
|
||||
|
||||
var database = db;
|
||||
|
||||
var self = this;
|
||||
|
||||
var existsDatabase = true;
|
||||
this.saveErrors = false;
|
||||
|
||||
// Only really needed for testing purposes
|
||||
Object.defineProperty(this, 'db', {
|
||||
set: function(val) {
|
||||
database = val;
|
||||
initialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
function sliceEnd(hasMore) {
|
||||
if (!hasMore) {
|
||||
// No more records present
|
||||
window.console.info('FB Migration: All records have been migrated');
|
||||
// Close the database
|
||||
database.close();
|
||||
if (!self.saveErrors) {
|
||||
var req = window.indexedDB.deleteDatabase(IDB_NAME);
|
||||
req.onsuccess = function() {
|
||||
window.console.info('FB Migration: ',
|
||||
'Now the indexedDB has been deleted');
|
||||
};
|
||||
req.onerror = function() {
|
||||
window.console.error('FB Migration: ',
|
||||
'Error while deleting indexedDB: ',
|
||||
req.error.name);
|
||||
};
|
||||
}
|
||||
else {
|
||||
window.console.warn('There are save errors. ', 'DB not deleted');
|
||||
}
|
||||
}
|
||||
if (typeof self.onfinished === 'function') {
|
||||
self.onfinished(hasMore, self.saveErrors);
|
||||
}
|
||||
}
|
||||
|
||||
this.migrateSlice = function migrateSlice() {
|
||||
var toBeMigrated = [];
|
||||
var pendingRecords = true;
|
||||
var self = this;
|
||||
|
||||
openIndexedDB(function() {
|
||||
var transaction = database.transaction([objStoreName], 'readwrite');
|
||||
var objectStore = transaction.objectStore(objStoreName);
|
||||
|
||||
var req = objectStore.openCursor();
|
||||
|
||||
req.onsuccess = function(e) {
|
||||
var cursor = e.target.result;
|
||||
if (cursor && toBeMigrated.length < SLICE_SIZE) {
|
||||
var obj = cursor.value;
|
||||
toBeMigrated.push(obj);
|
||||
cursor.continue();
|
||||
}
|
||||
else {
|
||||
if (!cursor) {
|
||||
pendingRecords = false;
|
||||
}
|
||||
migrateRecords(toBeMigrated, function() {
|
||||
sliceEnd(pendingRecords);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
window.console.error('FB Migration: ',
|
||||
'Error while requesting a cursor: ',
|
||||
req.error.name);
|
||||
if (typeof self.onerror === 'function') {
|
||||
self.onerror();
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
this.reset = function() {
|
||||
existsDatabase = true;
|
||||
initialized = false;
|
||||
this.saveErrors = false;
|
||||
database = null;
|
||||
};
|
||||
|
||||
function migrateRecords(list, cb) {
|
||||
if (list.length === 0) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
var numResponses = 0;
|
||||
var migrateCb = function migratCb() {
|
||||
numResponses++;
|
||||
if (numResponses === list.length) {
|
||||
cb();
|
||||
}
|
||||
};
|
||||
for (var j = 0; j < list.length; j++) {
|
||||
migrateRecord(list[j], migrateCb);
|
||||
}
|
||||
}
|
||||
|
||||
function removeRecord(obj, cb) {
|
||||
// Now deleting from indexedDB
|
||||
var trans = database.transaction([objStoreName], 'readwrite');
|
||||
var objectStore = trans.objectStore(objStoreName);
|
||||
|
||||
var req = objectStore.delete(obj.uid);
|
||||
req.onsuccess = cb;
|
||||
req.onerror = function() {
|
||||
window.console.error('FB Migration: Error while deleting indexedDB: ',
|
||||
obj.uid, ': ', req.error.name);
|
||||
cb();
|
||||
};
|
||||
}
|
||||
|
||||
function migrateRecord(obj, cb) {
|
||||
var req = fb.contacts.save(obj);
|
||||
|
||||
req.onsuccess = function() {
|
||||
window.console.info('FB Migration: indexed DB Record migrated: ',
|
||||
obj.uid);
|
||||
contacts.List.refreshFb(obj.uid);
|
||||
removeRecord(obj, cb);
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
window.console.error('FB Migration: Error while saving to datastore: ',
|
||||
obj.uid, req.error.name);
|
||||
if (req.error.name === 'AlreadyExists') {
|
||||
// Then that record was already migrated and it is safe to delete it
|
||||
removeRecord(obj, cb);
|
||||
}
|
||||
else {
|
||||
self.saveErrors = true;
|
||||
cb();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function openIndexedDB(cb) {
|
||||
if (database) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
var req = window.indexedDB.open(IDB_NAME);
|
||||
|
||||
req.onupgradeneeded = function() {
|
||||
existsDatabase = false;
|
||||
};
|
||||
|
||||
req.onsuccess = function() {
|
||||
if (!existsDatabase) {
|
||||
// Delete it as indexedDB will have created it
|
||||
window.console.warn('FB Migration: The database does not exist');
|
||||
window.indexedDB.deleteDatabase(IDB_NAME);
|
||||
|
||||
if (typeof self.onfinished === 'function') {
|
||||
self.onfinished(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
database = req.result;
|
||||
objStoreName = database.objectStoreNames[0];
|
||||
|
||||
cb();
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
window.console.error('Indexed DB cannot be opened: ', req.error.name);
|
||||
if (typeof self.onerror === 'function') {
|
||||
self.onerror();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var start = function start() {
|
||||
idleObserver = new IdleObserver();
|
||||
navigator.addIdleObserver(idleObserver);
|
||||
};
|
||||
|
||||
this.start = start;
|
||||
|
||||
var stop = function stop() {
|
||||
navigator.removeIdleObserver(idleObserver);
|
||||
if (migrator) {
|
||||
migrator.reset();
|
||||
}
|
||||
if (typeof self.onmigrationdone === 'function') {
|
||||
self.onmigrationdone();
|
||||
}
|
||||
};
|
||||
|
||||
this.stop = stop;
|
||||
|
||||
function onactive() {
|
||||
active = true;
|
||||
}
|
||||
|
||||
function sliceFinished(hasMore, saveErrors) {
|
||||
if (!hasMore && !saveErrors) {
|
||||
// And annotate in a cookie that migration was done
|
||||
utils.cookie.update({
|
||||
fbMigrated: true
|
||||
});
|
||||
stop();
|
||||
}
|
||||
else if (saveErrors && !hasMore) {
|
||||
// The process is finished for the moment though there are errors
|
||||
if (typeof self.onmigrationdone === 'function') {
|
||||
self.onmigrationdone(true);
|
||||
}
|
||||
ongoingMigration = false;
|
||||
}
|
||||
else {
|
||||
if (typeof self.onslicemigrated === 'function') {
|
||||
self.onslicemigrated();
|
||||
}
|
||||
if (!active) {
|
||||
ongoingMigration = true;
|
||||
// If still inactive next slice is migrated
|
||||
migrator.migrateSlice();
|
||||
}
|
||||
else {
|
||||
ongoingMigration = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var onidle = function onidle() {
|
||||
active = false;
|
||||
if (ongoingMigration) {
|
||||
return;
|
||||
}
|
||||
window.console.info('Idle event!!!. FB Migration about to start');
|
||||
ongoingMigration = true;
|
||||
if (!migrator) {
|
||||
migrator = new DatastoreMigrator(db);
|
||||
migrator.onfinished = sliceFinished;
|
||||
migrator.onerror = function() {
|
||||
ongoingMigration = false;
|
||||
migrator.reset();
|
||||
if (typeof self.onerror === 'function') {
|
||||
self.onerror({
|
||||
name: 'Migration Error'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
migrator.saveErrors = false;
|
||||
migrator.migrateSlice();
|
||||
};
|
||||
|
||||
// Mainly needed for testing purposes
|
||||
this.onidle = onidle;
|
||||
};
|
||||
54
gaia/apps/communications/contacts/js/fb/fb_init.js
Executable file
54
gaia/apps/communications/contacts/js/fb/fb_init.js
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
'use strict';
|
||||
|
||||
/* global utils */
|
||||
|
||||
var fb = window.fb || {};
|
||||
var config = window.config || {};
|
||||
|
||||
if (typeof fb.init === 'undefined') {
|
||||
(function() {
|
||||
var initialized = false;
|
||||
var initializing = false;
|
||||
var EV_FB_INIT = 'fb_init_initialized';
|
||||
|
||||
fb.isEnabled = false;
|
||||
|
||||
fb.init = function(callback) {
|
||||
if (initialized) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (initializing) {
|
||||
document.addEventListener(EV_FB_INIT, function handler(e) {
|
||||
initializing = false;
|
||||
document.removeEventListener(EV_FB_INIT, handler);
|
||||
callback();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
initializing = true;
|
||||
utils.config.load('/contacts/config.json').then(
|
||||
function cLoaded(configData) {
|
||||
if (configData.facebookEnabled === true) {
|
||||
fb.isEnabled = true;
|
||||
}
|
||||
|
||||
fb.operationsTimeout = config.operationsTimeout =
|
||||
configData.operationsTimeout;
|
||||
fb.logLevel = configData.logLevel || 'none';
|
||||
fb.syncPeriod = configData.facebookSyncPeriod || 24;
|
||||
fb.testToken = configData.testToken;
|
||||
|
||||
initialized = true;
|
||||
|
||||
document.dispatchEvent(new CustomEvent(EV_FB_INIT));
|
||||
|
||||
callback();
|
||||
}, function loadError(err) {
|
||||
console.error('Error while loading config.json', err);
|
||||
});
|
||||
};
|
||||
})();
|
||||
}
|
||||
677
gaia/apps/communications/contacts/js/fb/fb_link.js
Executable file
677
gaia/apps/communications/contacts/js/fb/fb_link.js
Executable file
|
|
@ -0,0 +1,677 @@
|
|||
'use strict';
|
||||
|
||||
/* global Curtain, FacebookConnector, ImageLoader, Normalizer, utils,
|
||||
ImportStatusData, oauth2 */
|
||||
|
||||
var fb = window.fb || {};
|
||||
|
||||
if (!fb.link) {
|
||||
(function(document) {
|
||||
// parameter id for the linking contact
|
||||
var link = fb.link = {};
|
||||
var UI = link.ui = {};
|
||||
|
||||
link.CID_PARAM = 'contactid';
|
||||
|
||||
// Contact id that the user wants to be linked to
|
||||
var contactid;
|
||||
// The data of such a contact
|
||||
var cdata;
|
||||
|
||||
// Access token
|
||||
var access_token;
|
||||
|
||||
// Used to control the number of queries
|
||||
var numQueries = 0;
|
||||
|
||||
// The uid of the friend to be linked
|
||||
var friendUidToLink;
|
||||
|
||||
// Base query to search for contacts
|
||||
var SEARCH_QUERY = ['SELECT uid, name, email from user ',
|
||||
' WHERE uid IN (SELECT uid1 FROM friend WHERE uid2=me()) ',
|
||||
' AND (', null, ')', ' ORDER BY name'
|
||||
];
|
||||
|
||||
// Conditions
|
||||
var MAIL_COND, CELL_COND;
|
||||
/* jshint ignore:start */
|
||||
MAIL_COND = ['strpos(email, ' , "'", null, "'", ') >= 0'];
|
||||
CELL_COND = ['strpos(cell, ' , "'", null, "'", ') >= 0'];
|
||||
/* jshint ignore:end */
|
||||
|
||||
var ALL_QUERY = ['SELECT uid, name, last_name, first_name,',
|
||||
' middle_name, email from user ',
|
||||
' WHERE uid IN (SELECT uid1 FROM friend WHERE uid2=me()) ',
|
||||
' ORDER BY name'
|
||||
];
|
||||
|
||||
var COUNT_QUERY = 'SELECT uid FROM user WHERE uid IN ' +
|
||||
'(SELECT uid1 FROM friend WHERE uid2=me())';
|
||||
|
||||
var SEARCH_ACCENTS_FIELDS = {
|
||||
'last_name': 'familyName',
|
||||
'first_name': 'givenName',
|
||||
'middle_name': 'givenName'
|
||||
};
|
||||
|
||||
var friendsList;
|
||||
var viewButton, mainSection;
|
||||
|
||||
var currentRecommendation = null;
|
||||
var allFriends = null;
|
||||
// Enables cancelation
|
||||
var currentNetworkRequest = null;
|
||||
// State can be proposal or view All
|
||||
var state;
|
||||
var imgLoader;
|
||||
|
||||
// Only needed for testing purposes
|
||||
var completedCb;
|
||||
|
||||
function notifyParent(message) {
|
||||
parent.postMessage({
|
||||
type: message.type || '',
|
||||
data: message.data || ''
|
||||
}, fb.CONTACTS_APP_ORIGIN);
|
||||
}
|
||||
|
||||
// Builds the first query for finding a contact to be linked to
|
||||
function buildQuery(contact) {
|
||||
var filter = [];
|
||||
|
||||
if (contact.tel && contact.tel.length > 0) {
|
||||
contact.tel.forEach(function(tel) {
|
||||
CELL_COND[2] = tel.value.trim();
|
||||
filter.push(CELL_COND.join(''));
|
||||
filter.push(' OR ');
|
||||
});
|
||||
}
|
||||
|
||||
if (contact.email && contact.email.length > 0) {
|
||||
contact.email.forEach(function(email) {
|
||||
MAIL_COND[2] = email.value.trim();
|
||||
filter.push(MAIL_COND.join(''));
|
||||
filter.push(' OR ');
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the last OR
|
||||
if (filter.length > 0) {
|
||||
filter[filter.length - 1] = null;
|
||||
}
|
||||
var filterStr = filter.join('');
|
||||
|
||||
var out;
|
||||
if (filterStr) {
|
||||
SEARCH_QUERY[3] = filterStr;
|
||||
out = SEARCH_QUERY.join('');
|
||||
}
|
||||
else {
|
||||
out = null;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// entry point for obtaining a remote proposal
|
||||
link.getRemoteProposal = function(acc_tk, contid) {
|
||||
var cid = contid || contactid;
|
||||
|
||||
var req = fb.utils.getContactData(cid);
|
||||
req.onsuccess = function() {
|
||||
if (req.result) {
|
||||
cdata = req.result;
|
||||
numQueries = 1;
|
||||
currentRecommendation = null;
|
||||
var query = buildQuery(cdata);
|
||||
// Check whether we have enough info for the first query
|
||||
// otherwise launch the getAll query
|
||||
if (query === null) {
|
||||
getRemoteProposalAll(acc_tk);
|
||||
return;
|
||||
}
|
||||
doGetRemoteProposal(acc_tk, cdata, query);
|
||||
}
|
||||
else {
|
||||
throw new Error(
|
||||
'FB: Contact to be linked not found in mozContacts: ' + cid);
|
||||
}
|
||||
};
|
||||
req.onerror = function(e) {
|
||||
window.console.error('FB: Error while retrieving contact data: ', cid);
|
||||
throw e;
|
||||
};
|
||||
};
|
||||
|
||||
function getRemoteProposalAll(acc_tk) {
|
||||
numQueries++;
|
||||
doGetRemoteProposal(acc_tk, null, ALL_QUERY.join(''), true);
|
||||
}
|
||||
|
||||
// This function deals with the response for a proposal
|
||||
// It takes care whether a response with the number of friends is present
|
||||
// and updates the cache accordingly
|
||||
function proposalReadyMultiple(done, error, response) {
|
||||
// If there is an error we just pass it upstream
|
||||
if (response.error) {
|
||||
done(response);
|
||||
return;
|
||||
} else if (!Array.isArray(response.data)) {
|
||||
error({
|
||||
name: 'QueryResponseError'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var friendList, totalFriends;
|
||||
if (response.data.length > 0 &&
|
||||
Array.isArray(response.data[0].fql_result_set)) {
|
||||
friendList = response.data[0].fql_result_set;
|
||||
if (response.data[1] &&
|
||||
Array.isArray(response.data[1].fql_result_set)) {
|
||||
totalFriends = response.data[1].fql_result_set.length;
|
||||
}
|
||||
}
|
||||
else {
|
||||
friendList = response.data;
|
||||
totalFriends = friendList.length;
|
||||
}
|
||||
|
||||
if (typeof totalFriends !== 'undefined') {
|
||||
fb.utils.setCachedNumFriends(totalFriends);
|
||||
}
|
||||
|
||||
done({
|
||||
data: friendList
|
||||
});
|
||||
}
|
||||
|
||||
// Performs all the work to obtain the remote proposal
|
||||
// the "isAll" parameter indicates that the query will get all friends
|
||||
function doGetRemoteProposal(acc_tk, contactData, query, isAll) {
|
||||
/*
|
||||
Phone.lookup was analysed but we were not happy about how it worked
|
||||
|
||||
var searchService = 'https://api.facebook.com/method/phonebook.lookup' +
|
||||
'?include_non_fb=false';
|
||||
var entries = [];
|
||||
entries.push(getEntry(contactData));
|
||||
var sentries = JSON.stringify(entries);
|
||||
|
||||
*/
|
||||
var theQuery = query;
|
||||
var callback = proposalReadyMultiple.bind(null, fb.link.proposalReady,
|
||||
fb.link.errorHandler);
|
||||
// If the query is not going to provide a full list of friends
|
||||
// We count all in order to refresh the total number of friends
|
||||
if (!isAll) {
|
||||
theQuery = JSON.stringify({
|
||||
query1: query,
|
||||
query2: COUNT_QUERY
|
||||
});
|
||||
}
|
||||
|
||||
state = 'proposal';
|
||||
currentNetworkRequest = fb.utils.runQuery(theQuery, {
|
||||
success: callback,
|
||||
error: fb.link.errorHandler,
|
||||
timeout: fb.link.timeoutHandler
|
||||
}, acc_tk);
|
||||
}
|
||||
|
||||
// Invoked when remoteAll is canceled
|
||||
function cancelCb(shouldNotifyParent) {
|
||||
if (currentNetworkRequest) {
|
||||
currentNetworkRequest.cancel();
|
||||
currentNetworkRequest = null;
|
||||
}
|
||||
|
||||
Curtain.hide(shouldNotifyParent ? notifyParent.bind(
|
||||
null, {type: 'abort'}) : null);
|
||||
}
|
||||
|
||||
// Obtains a proposal with all friends
|
||||
function getRemoteAll() {
|
||||
Curtain.oncancel = cancelCb;
|
||||
Curtain.show('wait', 'friends');
|
||||
|
||||
state = 'friends';
|
||||
|
||||
currentNetworkRequest = fb.utils.runQuery(ALL_QUERY.join(''), {
|
||||
success: fb.link.friendsReady,
|
||||
error: fb.link.errorHandler,
|
||||
timeout: fb.link.timeoutHandler
|
||||
}, access_token);
|
||||
}
|
||||
|
||||
// Obtains a linking proposal to be shown to the user
|
||||
link.getProposal = function(cid, acc_tk) {
|
||||
link.getRemoteProposal(acc_tk, cid);
|
||||
};
|
||||
|
||||
// Executed when the server response is available
|
||||
link.proposalReady = function(response) {
|
||||
if (typeof response.error !== 'undefined') {
|
||||
window.console.error('FB: Error while retrieving link data',
|
||||
response.error.code, response.error.message);
|
||||
|
||||
if (response.error.code != 190) {
|
||||
setCurtainHandlersErrorProposal();
|
||||
Curtain.show('error', 'proposal');
|
||||
}
|
||||
else {
|
||||
// access token problem. A new OAuth 2.0 flow has to start
|
||||
handleTokenError();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data.length === 0 && numQueries === 1) {
|
||||
getRemoteProposalAll(access_token);
|
||||
}
|
||||
else {
|
||||
var data = response.data;
|
||||
currentRecommendation = data;
|
||||
|
||||
var sortedByName = [];
|
||||
var numFriendsProposed = data.length;
|
||||
var searchAccentsArrays = {};
|
||||
var index = 0;
|
||||
|
||||
data.forEach(function(item) {
|
||||
if (!item.email) {
|
||||
item.email = '';
|
||||
}
|
||||
var box = utils.misc.getPreferredPictureBox();
|
||||
item.picwidth = box.width;
|
||||
item.picheight = box.height;
|
||||
|
||||
// Only do this if we need to prepare the search accents phase
|
||||
if (numQueries === 2) {
|
||||
// Saving the original order
|
||||
sortedByName.push(item);
|
||||
// Populate the arrays for doing the accents related search
|
||||
Object.keys(SEARCH_ACCENTS_FIELDS).forEach(function(field) {
|
||||
searchAccentsArrays[field] = searchAccentsArrays[field] || [];
|
||||
if (item[field]) {
|
||||
// The different words for each item
|
||||
var words = item[field].split(/[ ]+/);
|
||||
// The whole word itself is added
|
||||
words.push(item[field]);
|
||||
words.forEach(function(word) {
|
||||
var obj = {
|
||||
originalIndex: index
|
||||
};
|
||||
obj[field] = Normalizer.toAscii(word).toLowerCase();
|
||||
searchAccentsArrays[field].push(obj);
|
||||
});
|
||||
}
|
||||
});
|
||||
index++;
|
||||
}
|
||||
});
|
||||
|
||||
if (numQueries === 2) {
|
||||
var accentsProposal = searchAccents(searchAccentsArrays, cdata);
|
||||
if (accentsProposal.length === 0) {
|
||||
data = sortedByName;
|
||||
mainSection.classList.add('no-proposal');
|
||||
numFriendsProposed = 0;
|
||||
}
|
||||
else {
|
||||
currentRecommendation = [];
|
||||
accentsProposal.forEach(function(proposalIndex) {
|
||||
currentRecommendation.push(sortedByName[proposalIndex]);
|
||||
});
|
||||
numFriendsProposed = currentRecommendation.length;
|
||||
}
|
||||
} else {
|
||||
viewButton.setAttribute('data-l10n-id', 'viewAll');
|
||||
viewButton.onclick = UI.viewAllFriends;
|
||||
}
|
||||
|
||||
utils.templates.append('#friends-list', currentRecommendation);
|
||||
imgLoader.reload();
|
||||
|
||||
if (typeof completedCb === 'function') {
|
||||
completedCb();
|
||||
}
|
||||
|
||||
Curtain.hide(function onCurtainHide() {
|
||||
sendReadyEvent();
|
||||
window.addEventListener('message', function linkOnViewPort(e) {
|
||||
if (e.origin !== fb.CONTACTS_APP_ORIGIN) {
|
||||
return;
|
||||
}
|
||||
var data = e.data;
|
||||
if (data && data.type === 'dom_transition_end') {
|
||||
window.removeEventListener('message', linkOnViewPort);
|
||||
utils.status.show({
|
||||
id: 'linkProposal',
|
||||
args: {
|
||||
numFriends: numFriendsProposed
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// This function needs to be called because link proposals through FB Query
|
||||
// do not work properly with words with special characters (bug 796714)
|
||||
function searchAccents(searchArrays, contactData) {
|
||||
var out = [];
|
||||
var searchFields = Object.keys(SEARCH_ACCENTS_FIELDS);
|
||||
|
||||
function compareItems(target, item) {
|
||||
var out;
|
||||
|
||||
if (typeof target === 'string') {
|
||||
out = target.localeCompare(item);
|
||||
if (out !== 0) {
|
||||
if (item.indexOf(target) === 0) {
|
||||
out = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
} // compareItems Function
|
||||
|
||||
searchFields.forEach(function(searchField) {
|
||||
// The array is ordered according to the search field
|
||||
// this enables an efficient binary search
|
||||
var searchArray = searchArrays[searchField].sort(function(a, b) {
|
||||
return a[searchField].localeCompare(b[searchField]);
|
||||
});
|
||||
|
||||
var fieldToSearch = contactData[SEARCH_ACCENTS_FIELDS[searchField]];
|
||||
if (fieldToSearch && fieldToSearch[0]) {
|
||||
// Splitting in the different words
|
||||
var dataToSearch = fieldToSearch[0].trim().split(/[ ]+/);
|
||||
|
||||
dataToSearch.forEach(function(aData) {
|
||||
var targetString = Normalizer.toAscii(aData).toLowerCase();
|
||||
var searchResult = utils.binarySearch(targetString, searchArray, {
|
||||
arrayField: searchField,
|
||||
compareFunction: compareItems
|
||||
});
|
||||
|
||||
searchResult.forEach(function(aResult) {
|
||||
var candidate = searchArray[aResult].originalIndex;
|
||||
// Avoiding to show two times the same result element
|
||||
if (out.indexOf(candidate) === -1) {
|
||||
out.push(candidate);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function sendReadyEvent() {
|
||||
parent.postMessage({
|
||||
type: 'ready', data: ''
|
||||
}, fb.CONTACTS_APP_ORIGIN);
|
||||
}
|
||||
|
||||
function setCurtainHandlersErrorProposal() {
|
||||
Curtain.oncancel = function() {
|
||||
cancelCb(true);
|
||||
};
|
||||
|
||||
Curtain.onretry = function getRemoteProposal() {
|
||||
Curtain.oncancel = function() {
|
||||
cancelCb(true);
|
||||
};
|
||||
Curtain.show('wait', state);
|
||||
|
||||
link.getRemoteProposal(access_token, contactid);
|
||||
};
|
||||
}
|
||||
|
||||
link.baseHandler = function(type) {
|
||||
if (state === 'friends') {
|
||||
Curtain.onretry = getRemoteAll;
|
||||
Curtain.oncancel = Curtain.hide;
|
||||
} else if (state === 'proposal') {
|
||||
setCurtainHandlersErrorProposal();
|
||||
}
|
||||
else if (state === 'linking') {
|
||||
Curtain.onretry = function retry_link() {
|
||||
Curtain.show('message', 'linking');
|
||||
UI.selected({
|
||||
target: {
|
||||
dataset: {
|
||||
uuid: friendUidToLink
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
Curtain.oncancel = Curtain.hide;
|
||||
}
|
||||
|
||||
Curtain.show(type, state);
|
||||
};
|
||||
|
||||
link.timeoutHandler = function() {
|
||||
link.baseHandler('timeout');
|
||||
};
|
||||
|
||||
link.errorHandler = function() {
|
||||
link.baseHandler('error');
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the list of contacts
|
||||
*
|
||||
*/
|
||||
function clearList() {
|
||||
if (!friendsList) {
|
||||
friendsList = document.querySelector('#friends-list');
|
||||
}
|
||||
var template = friendsList.querySelector('[data-template]');
|
||||
|
||||
utils.dom.removeChildNodes(friendsList);
|
||||
friendsList.appendChild(template);
|
||||
}
|
||||
|
||||
link.friendsReady = function(response) {
|
||||
if (typeof response.error === 'undefined' && response.data) {
|
||||
viewButton.setAttribute('data-l10n-id', 'viewRecommend');
|
||||
viewButton.onclick = UI.viewRecommended;
|
||||
|
||||
allFriends = response;
|
||||
|
||||
fb.utils.setCachedNumFriends(response.data.length);
|
||||
|
||||
response.data.forEach(function(item) {
|
||||
if (!item.email) {
|
||||
item.email = '';
|
||||
}
|
||||
});
|
||||
|
||||
clearList();
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
utils.templates.append(friendsList, response.data, fragment);
|
||||
friendsList.appendChild(fragment);
|
||||
|
||||
imgLoader.reload();
|
||||
|
||||
Curtain.hide();
|
||||
}
|
||||
else {
|
||||
window.console.error('FB: Error while retrieving friends',
|
||||
response.error.code, response.error.message);
|
||||
|
||||
if (response.error.code !== 190) {
|
||||
Curtain.onretry = getRemoteAll;
|
||||
Curtain.oncancel = Curtain.hide;
|
||||
}
|
||||
else {
|
||||
// Error with the access token. It is very unlikely that this happens
|
||||
Curtain.onretry = handleTokenError;
|
||||
}
|
||||
Curtain.show('error', 'friends');
|
||||
}
|
||||
};
|
||||
|
||||
function setCurtainHandlers() {
|
||||
Curtain.oncancel = function curtain_cancel() {
|
||||
cancelCb(true);
|
||||
};
|
||||
}
|
||||
|
||||
link.start = function(contactId, acc_tk, endCb) {
|
||||
// Only needed for testing purposes
|
||||
completedCb = endCb;
|
||||
|
||||
access_token = acc_tk;
|
||||
contactid = contactId;
|
||||
|
||||
setCurtainHandlers();
|
||||
clearList();
|
||||
imgLoader = new ImageLoader('#mainContent',
|
||||
'li:not([data-uuid="#uid#"])');
|
||||
|
||||
if (!acc_tk) {
|
||||
oauth2.getAccessToken(function proposal_new_token(new_acc_tk) {
|
||||
access_token = new_acc_tk;
|
||||
link.getProposal(contactId, new_acc_tk);
|
||||
}, 'proposal', 'facebook');
|
||||
}
|
||||
else {
|
||||
link.getProposal(contactId, acc_tk);
|
||||
}
|
||||
};
|
||||
|
||||
link.init = function() {
|
||||
mainSection = document.querySelector('#main');
|
||||
viewButton = document.querySelector('#view-all');
|
||||
};
|
||||
|
||||
function retryOnErrorCb() {
|
||||
UI.selected({
|
||||
target: {
|
||||
dataset: {
|
||||
uuid: friendUidToLink
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleTokenError() {
|
||||
Curtain.hide(notifyParent.bind(null, {
|
||||
type: 'token_error'
|
||||
}));
|
||||
var cb = function() {
|
||||
allFriends = null;
|
||||
link.start(contactid);
|
||||
};
|
||||
ImportStatusData.remove(fb.utils.TOKEN_DATA_KEY).then(cb);
|
||||
}
|
||||
|
||||
UI.selected = function(event) {
|
||||
Curtain.show('message', 'linking');
|
||||
|
||||
var element = event.target.parentNode;
|
||||
friendUidToLink = element.dataset.uuid;
|
||||
|
||||
// First it is needed to check whether is an already imported friend
|
||||
var req = fb.contacts.get(friendUidToLink);
|
||||
req.onsuccess = function() {
|
||||
if (req.result) {
|
||||
window.setTimeout(function delay() {
|
||||
Curtain.hide(notifyParent.bind(null, {
|
||||
type: 'item_selected',
|
||||
data: {
|
||||
uid: friendUidToLink
|
||||
}
|
||||
}));
|
||||
}, 1000);
|
||||
}
|
||||
else {
|
||||
state = 'linking';
|
||||
var callbacks = { };
|
||||
|
||||
callbacks.success = function(data) {
|
||||
Curtain.hide(notifyParent.bind(null, {
|
||||
type: 'item_selected',
|
||||
data: data
|
||||
}));
|
||||
};
|
||||
|
||||
callbacks.error = function(e) {
|
||||
var error = e.target.error;
|
||||
window.console.error('FB: Error while importing friend data ',
|
||||
JSON.stringify(error));
|
||||
Curtain.oncancel = Curtain.hide;
|
||||
if (error.code != 190) {
|
||||
Curtain.onretry = retryOnErrorCb;
|
||||
}
|
||||
else {
|
||||
Curtain.onretry = handleTokenError;
|
||||
}
|
||||
Curtain.show('error', 'linking');
|
||||
};
|
||||
|
||||
callbacks.timeout = function() {
|
||||
link.baseHandler('timeout');
|
||||
};
|
||||
|
||||
imgLoader.unload(); // Removing listeners
|
||||
FacebookConnector.importContact(friendUidToLink, access_token,
|
||||
callbacks, 'not_match');
|
||||
}
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
window.console.error('FB: Error while importing friend data');
|
||||
Curtain.oncancel = Curtain.hide;
|
||||
Curtain.onretry = retryOnErrorCb;
|
||||
Curtain.show('error', 'linking');
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
UI.end = function(event) {
|
||||
var msg = {
|
||||
type: 'window_close',
|
||||
data: ''
|
||||
};
|
||||
|
||||
parent.postMessage(msg, fb.CONTACTS_APP_ORIGIN);
|
||||
};
|
||||
|
||||
UI.viewAllFriends = function(event) {
|
||||
if (!allFriends) {
|
||||
getRemoteAll();
|
||||
}
|
||||
else {
|
||||
link.friendsReady(allFriends);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
UI.viewRecommended = function(event) {
|
||||
// event.target === viewButton
|
||||
event.target.onclick = UI.viewAllFriends;
|
||||
event.target.setAttribute('data-l10n-id', 'viewAll');
|
||||
|
||||
clearList();
|
||||
utils.templates.append(friendsList, currentRecommendation);
|
||||
imgLoader.reload();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
})(document);
|
||||
}
|
||||
43
gaia/apps/communications/contacts/js/fb/fb_link_init.js
Executable file
43
gaia/apps/communications/contacts/js/fb/fb_link_init.js
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
'use strict';
|
||||
|
||||
/* global fb, utils */
|
||||
|
||||
(function(document) {
|
||||
var cid = window.location.search.substring(fb.link.CID_PARAM.length + 2);
|
||||
|
||||
// Getting the timeout config from the parent
|
||||
if (parent.fb) {
|
||||
fb.operationsTimeout = parent.fb.operationsTimeout;
|
||||
}
|
||||
|
||||
utils.listeners.add({
|
||||
'#link-header': [
|
||||
{
|
||||
event: 'action',
|
||||
handler: fb.link.ui.end
|
||||
}
|
||||
],
|
||||
'#friends-list': fb.link.ui.selected
|
||||
});
|
||||
|
||||
fb.link.init();
|
||||
|
||||
// This event listener is added manually as it wil be changing dynamically
|
||||
document.querySelector('#view-all').onclick = fb.link.ui.viewAllFriends;
|
||||
|
||||
// Module fb.contacts is initialized just in case we need it
|
||||
fb.contacts.init(function fb_init() {
|
||||
window.addEventListener('message', function getAccessToken(e) {
|
||||
if (e.origin !== fb.CONTACTS_APP_ORIGIN) {
|
||||
return;
|
||||
}
|
||||
window.removeEventListener('message', getAccessToken);
|
||||
fb.link.start(cid, e.data.data);
|
||||
});
|
||||
|
||||
parent.postMessage({
|
||||
type: 'messaging_ready',
|
||||
data: ''
|
||||
}, fb.CONTACTS_APP_ORIGIN);
|
||||
});
|
||||
})(document);
|
||||
110
gaia/apps/communications/contacts/js/fb/fb_messaging.js
Executable file
110
gaia/apps/communications/contacts/js/fb/fb_messaging.js
Executable file
|
|
@ -0,0 +1,110 @@
|
|||
'use strict';
|
||||
|
||||
/* global LazyLoader */
|
||||
/* global oauth2 */
|
||||
/* global oauthflow */
|
||||
|
||||
var fb = window.fb || {};
|
||||
|
||||
if (typeof fb.msg === 'undefined') {
|
||||
LazyLoader.load('/shared/pages/import/js/parameters.js', () => {
|
||||
(function(document) {
|
||||
var Msg = fb.msg = {};
|
||||
var to;
|
||||
var message;
|
||||
var params = oauthflow.params.facebook;
|
||||
var appId = params.applicationId;
|
||||
var redirectURI = params.redirectMsg;
|
||||
|
||||
Msg.CID_PARAM = 'contactid';
|
||||
|
||||
// Only needed if we decide to craft our own UI
|
||||
Msg.wallPost = function(uid, msg) {
|
||||
to = uid || to;
|
||||
message = msg || message;
|
||||
oauth2.getAccessToken(doWallPost, 'wallPost', 'facebook');
|
||||
};
|
||||
|
||||
Msg.sendPrivate = function(uid, msg) {
|
||||
// TODO: To be implemented (if we decide to craft our custom UI)
|
||||
};
|
||||
|
||||
// This code is for posting to user's wall. Only will be necessary if
|
||||
// we decide to craft our own UI for posting to the wall
|
||||
function doWallPost(token) {
|
||||
var msgWallService = 'https://graph.facebook.com/#/feed?method=POST';
|
||||
|
||||
msgWallService = msgWallService.replace(/#/, to);
|
||||
|
||||
var params = [
|
||||
'access_token' + '=' + token,
|
||||
'message' + '=' + message,
|
||||
'callback' + '=' + 'fb.msg.ui.wallPosted'
|
||||
];
|
||||
|
||||
var q = params.join('&');
|
||||
|
||||
var jsonp = document.createElement('script');
|
||||
jsonp.src = msgWallService + '&' + q;
|
||||
|
||||
document.body.appendChild(jsonp);
|
||||
}
|
||||
|
||||
var UI = Msg.ui = {};
|
||||
|
||||
function getFbContactUid(contactId, callback) {
|
||||
var req = fb.utils.getContactData(contactId);
|
||||
|
||||
req.onsuccess = function() {
|
||||
var fbContact = new fb.Contact(req.result);
|
||||
callback(fbContact.uid);
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
window.console.error('Contacts: Contact not found!');
|
||||
callback(null);
|
||||
};
|
||||
}
|
||||
|
||||
function openMsgDialog(dialogURI, uid) {
|
||||
var params = [
|
||||
'app_id' + '=' + appId,
|
||||
'to' + '=' + uid,
|
||||
'redirect_uri' + '=' + encodeURIComponent(redirectURI)
|
||||
];
|
||||
|
||||
var target = dialogURI + params.join('&');
|
||||
window.open(fb.utils.getNonCacheableUrl(target), '', 'dialog');
|
||||
}
|
||||
|
||||
// Use the FB Dialogs functionality for posting to the wall
|
||||
UI.wallPost = function(contactId) {
|
||||
getFbContactUid(contactId, function ui_wallPost(uid) {
|
||||
if (uid) {
|
||||
var dialogURI = 'https://m.facebook.com/dialog/feed?';
|
||||
openMsgDialog(dialogURI, uid);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Use a Web view from Facebook for sending private messages
|
||||
// TODO: Check with Facebook why the send message dialog seems not to be
|
||||
// working on mobile
|
||||
UI.sendPrivateMsg = function(contactId) {
|
||||
getFbContactUid(contactId, function ui_sendMsg(uid) {
|
||||
if (uid) {
|
||||
var url = 'https://m.facebook.com/compose_message/?uid=' + uid;
|
||||
window.open(fb.utils.getNonCacheableUrl(url),
|
||||
'', 'dialog');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
UI.wallPosted = function(result) {
|
||||
UI.end();
|
||||
window.console.log(JSON.stringify(result));
|
||||
};
|
||||
|
||||
})(document);
|
||||
});
|
||||
}
|
||||
55
gaia/apps/communications/contacts/js/fb_loader.js
Executable file
55
gaia/apps/communications/contacts/js/fb_loader.js
Executable file
|
|
@ -0,0 +1,55 @@
|
|||
'use strict';
|
||||
/* global LazyLoader */
|
||||
/* exported fbLoader */
|
||||
|
||||
var fbLoader = (function() {
|
||||
|
||||
var loaded = false;
|
||||
|
||||
var loadFb = function loadFb() {
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
var iframesFragment = document.createDocumentFragment();
|
||||
|
||||
var oauth = document.createElement('iframe');
|
||||
oauth.id = 'iframe_oauth';
|
||||
oauth.hidden = true;
|
||||
iframesFragment.appendChild(oauth);
|
||||
|
||||
var extensions = document.createElement('iframe');
|
||||
extensions.id = 'iframe_extensions';
|
||||
iframesFragment.appendChild(extensions);
|
||||
|
||||
document.body.appendChild(iframesFragment);
|
||||
|
||||
var scripts = [
|
||||
'/shared/js/contacts/import/utilities/misc.js',
|
||||
'/shared/js/contacts/import/import_status_data.js',
|
||||
'/contacts/js/service_extensions.js',
|
||||
'/shared/pages/import/js/parameters.js',
|
||||
'/shared/js/fb/fb_request.js',
|
||||
'/shared/js/contacts/import/facebook/fb_data.js',
|
||||
'/shared/js/contacts/import/facebook/fb_utils.js',
|
||||
'/shared/js/contacts/import/facebook/fb_query.js',
|
||||
'/shared/js/fb/fb_reader_utils.js',
|
||||
'/shared/js/contacts/import/facebook/fb_contact_utils.js',
|
||||
'/shared/js/contacts/import/facebook/fb_contact.js',
|
||||
'/contacts/js/fb/fb_link.js',
|
||||
'/contacts/js/fb/fb_messaging.js'
|
||||
];
|
||||
|
||||
LazyLoader.load(scripts, function() {
|
||||
var event = new CustomEvent('facebookLoaded');
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
load: loadFb,
|
||||
get loaded() { return loaded; }
|
||||
};
|
||||
|
||||
})();
|
||||
67
gaia/apps/communications/contacts/js/fb_resolver.js
Executable file
67
gaia/apps/communications/contacts/js/fb_resolver.js
Executable file
|
|
@ -0,0 +1,67 @@
|
|||
'use strict';
|
||||
/* global contacts */
|
||||
/* global LazyLoader */
|
||||
|
||||
var fb = window.fb || {};
|
||||
|
||||
fb.resolver = function(item, loader) {
|
||||
var FB_SCRIPTS_NEEDED = [
|
||||
'/shared/js/fb/fb_request.js',
|
||||
'/shared/js/fb/fb_data_reader.js',
|
||||
'/shared/js/fb/fb_reader_utils.js'
|
||||
];
|
||||
|
||||
var status = item.dataset.status;
|
||||
var isFbContact = 'fbUid' in item.dataset;
|
||||
var EMPTY_OBJ = {};
|
||||
|
||||
if (isFbContact && status !== 'pending' && status !== 'loaded') {
|
||||
item.dataset.status = 'pending';
|
||||
|
||||
LazyLoader.load(FB_SCRIPTS_NEEDED, function() {
|
||||
var fbReq = fb.contacts.get(item.dataset.fbUid);
|
||||
fbReq.onsuccess = function() {
|
||||
var fbData = fbReq.result;
|
||||
if (fbData) {
|
||||
var id = item.dataset.uuid;
|
||||
var hasPhoto = contacts.List.hasPhoto(id);
|
||||
var renderPhoto = false;
|
||||
if (!hasPhoto) {
|
||||
renderPhoto = contacts.List.updatePhoto(fbData, id);
|
||||
} else {
|
||||
renderPhoto = true;
|
||||
}
|
||||
if (renderPhoto) {
|
||||
contacts.List.renderPhoto(item, id);
|
||||
document.dispatchEvent(new CustomEvent('onupdate'));
|
||||
}
|
||||
|
||||
item.dataset.status = 'loaded';
|
||||
|
||||
// The organization is also loaded
|
||||
var contactObj = EMPTY_OBJ;
|
||||
// If there is no local org we render the FB org
|
||||
if (!item.querySelector('span.org')) {
|
||||
var org = fbData.org;
|
||||
if (Array.isArray(org)) {
|
||||
contactObj = {
|
||||
org: org
|
||||
};
|
||||
}
|
||||
}
|
||||
contacts.List.renderFbData(contactObj, item);
|
||||
} else {
|
||||
window.console.warn('FB Data could not be retrieved by the resolver',
|
||||
'for UID: ', item.dataset.fbUid);
|
||||
item.dataset.status = 'loaded';
|
||||
}
|
||||
};
|
||||
|
||||
fbReq.onerror = function() {
|
||||
item.dataset.status = 'error';
|
||||
};
|
||||
});
|
||||
} else if (status === 'loaded' || !isFbContact) {
|
||||
loader.defaultLoad(item);
|
||||
}
|
||||
};
|
||||
80
gaia/apps/communications/contacts/js/header_ui.js
Executable file
80
gaia/apps/communications/contacts/js/header_ui.js
Executable file
|
|
@ -0,0 +1,80 @@
|
|||
/* globals ActivityHandler, MainNavigation */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
exports.HeaderUI = {
|
||||
_lastCustomHeaderCallback: null,
|
||||
settings: document.getElementById('view-settings'),
|
||||
settingsButton: document.getElementById('settings-button'),
|
||||
header: document.getElementById('contacts-list-header'),
|
||||
addButton: document.getElementById('add-contact-button'),
|
||||
editModeTitleElement: document.getElementById('edit-title'),
|
||||
appTitleElement: document.getElementById('app-title'),
|
||||
|
||||
setupActionableHeader: function() {
|
||||
this.header.removeAttribute('action');
|
||||
this.settingsButton.hidden = false;
|
||||
this.addButton.hidden = false;
|
||||
|
||||
this.appTitleElement.setAttribute('data-l10n-id', 'contacts');
|
||||
},
|
||||
|
||||
setCancelableHeader: function(cb, titleId) {
|
||||
this.setupCancelableHeader(titleId);
|
||||
this.header.removeEventListener('action', this.handleCancel);
|
||||
this._lastCustomHeaderCallback = cb;
|
||||
this.header.addEventListener('action', cb);
|
||||
},
|
||||
|
||||
setNormalHeader: function() {
|
||||
this.setupActionableHeader();
|
||||
this.header.removeEventListener('action', this._lastCustomHeaderCallback);
|
||||
this.header.addEventListener('action', this.handleCancel);
|
||||
},
|
||||
|
||||
setupCancelableHeader: function(alternativeTitle) {
|
||||
this.header.setAttribute('action', 'close');
|
||||
this.settingsButton.hidden = true;
|
||||
this.addButton.hidden = true;
|
||||
if (alternativeTitle) {
|
||||
this.appTitleElement.setAttribute('data-l10n-id', alternativeTitle);
|
||||
}
|
||||
// Trigger the title to re-run font-fit/centering logic
|
||||
this.appTitleElement.textContent = this.appTitleElement.textContent;
|
||||
},
|
||||
|
||||
handleCancel: function() {
|
||||
//If in an activity, cancel it
|
||||
if (ActivityHandler.currentlyHandling) {
|
||||
ActivityHandler.postCancel();
|
||||
MainNavigation.home();
|
||||
} else {
|
||||
MainNavigation.back();
|
||||
}
|
||||
},
|
||||
|
||||
updateSelectCountTitle: function(count) {
|
||||
navigator.mozL10n.setAttributes(this.editModeTitleElement,
|
||||
'SelectedTxt',
|
||||
{n: count});
|
||||
},
|
||||
|
||||
hideAddButton: function() {
|
||||
this.addButton.classList.add('hide');
|
||||
},
|
||||
|
||||
updateHeader: function(isCancelable) {
|
||||
if (isCancelable) {
|
||||
var alternativeTitle = null;
|
||||
var activityName = ActivityHandler.activityName;
|
||||
if (activityName === 'pick' || activityName === 'update') {
|
||||
alternativeTitle = 'selectContact';
|
||||
}
|
||||
this.setupCancelableHeader(alternativeTitle);
|
||||
} else {
|
||||
this.setupActionableHeader();
|
||||
}
|
||||
}
|
||||
};
|
||||
})(window);
|
||||
122
gaia/apps/communications/contacts/js/loader.js
Executable file
122
gaia/apps/communications/contacts/js/loader.js
Executable file
|
|
@ -0,0 +1,122 @@
|
|||
(function(exports) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/* global LazyLoader */
|
||||
/* exported Loader */
|
||||
|
||||
// Loader allow to load "views" and "utilities" in a easy form.
|
||||
// We load with this class the DOM dependencies and the libraries
|
||||
// needed to use the view/utility.
|
||||
|
||||
var SHARED = 'shared';
|
||||
var SHARED_PATH = '/' + SHARED + '/' + 'js';
|
||||
|
||||
var SHARED_UTILS = 'sharedUtilities';
|
||||
var SHARED_UTILS_PATH = SHARED_PATH + '/contacts/import/utilities';
|
||||
|
||||
var SHARED_CONTACTS = 'sharedContacts';
|
||||
var SHARED_CONTACTS_PATH = SHARED_PATH + '/' + 'contacts';
|
||||
|
||||
/**
|
||||
* Specifies dependencies for resources
|
||||
* E.g., mapping Facebook as a dependency of views
|
||||
*/
|
||||
var dependencies = {
|
||||
views: {
|
||||
Search: function(callback) {
|
||||
LazyLoader.load(SHARED_PATH + '/utilities.js', callback);
|
||||
}
|
||||
},
|
||||
utilities: {},
|
||||
sharedUtilities: {}
|
||||
};
|
||||
|
||||
// Mapping of view names to element IDs
|
||||
// TODO: Having a more standardized way of specifying this would be nice.
|
||||
// Then we could get rid of this mapping entirely
|
||||
// E.g., #details-view, #list-view, #form-view
|
||||
var elementMapping = {
|
||||
details: ['view-contact-details'],
|
||||
form: ['view-contact-form'],
|
||||
settings: ['settings-wrapper'],
|
||||
search: ['search-view', SHARED_CONTACTS],
|
||||
multiple_select: ['multiple-select-view'],
|
||||
overlay: ['loading-overlay', SHARED_UTILS],
|
||||
ice: ['ice-view'],
|
||||
import_sim_contacts: [null, SHARED_UTILS]
|
||||
};
|
||||
|
||||
function load(type, file, callback) {
|
||||
/**
|
||||
* Performs the actual lazy loading
|
||||
* Called once all dependencies are met
|
||||
*/
|
||||
function doLoad() {
|
||||
var name = file.toLowerCase();
|
||||
var path = null;
|
||||
if (elementMapping[name] && elementMapping[name][1]) {
|
||||
path = elementMapping[name][1];
|
||||
}
|
||||
var finalPath = 'js' + '/' + type;
|
||||
|
||||
switch (path) {
|
||||
case SHARED:
|
||||
finalPath = SHARED_PATH;
|
||||
break;
|
||||
case SHARED_UTILS:
|
||||
finalPath = SHARED_UTILS_PATH;
|
||||
break;
|
||||
case SHARED_CONTACTS:
|
||||
finalPath = SHARED_CONTACTS_PATH;
|
||||
break;
|
||||
default:
|
||||
finalPath = 'js' + '/' + type;
|
||||
}
|
||||
|
||||
var toLoad = [finalPath + '/' + name + '.js'];
|
||||
if (elementMapping[name] && elementMapping[name][0]) {
|
||||
var node = document.getElementById(elementMapping[name][0]);
|
||||
if (node) {
|
||||
toLoad.unshift(node);
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader.load(toLoad, function() {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (dependencies[type][file]) {
|
||||
return dependencies[type][file](doLoad);
|
||||
}
|
||||
|
||||
doLoad();
|
||||
}
|
||||
|
||||
var Loader = {
|
||||
|
||||
/**
|
||||
* Loads a view from the views/ folder
|
||||
* @param {String} view name.
|
||||
* @param {Function} callback.
|
||||
*/
|
||||
view: function loadView(view, callback) {
|
||||
load('views', view, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads a utility from the utilities/ folder
|
||||
* @param {String} utility name.
|
||||
* @param {Function} callback.
|
||||
*/
|
||||
utility: function loadUtility(utility, callback) {
|
||||
load('utilities', utility, callback);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
exports.Loader = Loader;
|
||||
}(window));
|
||||
7
gaia/apps/communications/contacts/js/main_navigation.js
Executable file
7
gaia/apps/communications/contacts/js/main_navigation.js
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
/* global navigationStack */
|
||||
/* exported MainNavigation */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
exports.MainNavigation = new navigationStack('view-contacts-list');
|
||||
}(window));
|
||||
115
gaia/apps/communications/contacts/js/match_service.js
Executable file
115
gaia/apps/communications/contacts/js/match_service.js
Executable file
|
|
@ -0,0 +1,115 @@
|
|||
'use strict';
|
||||
/*
|
||||
* This code is in charge of launching a diferent window
|
||||
* to handle if one or more contacts have the same info
|
||||
* that the one we are trying to save.
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
var matchService = {};
|
||||
|
||||
const CONTACTS_APP_ORIGIN = location.origin;
|
||||
|
||||
var matcherWindow = null;
|
||||
var matcherFrame = null;
|
||||
var currentURL;
|
||||
|
||||
function openWindow(url) {
|
||||
if (!url || matcherWindow !== null) {
|
||||
return;
|
||||
}
|
||||
if (window.VulpesCompat) {
|
||||
// Keep Gaia's real duplicate-management view inside the Contacts app.
|
||||
// It communicates with this window through the original postMessage API.
|
||||
matcherFrame = document.createElement('iframe');
|
||||
matcherFrame.title = 'Contacts';
|
||||
matcherFrame.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;border:0;z-index:1000;background:white';
|
||||
matcherFrame.src = url;
|
||||
document.body.appendChild(matcherFrame);
|
||||
matcherWindow = matcherFrame.contentWindow;
|
||||
} else {
|
||||
matcherWindow = window.open(url);
|
||||
}
|
||||
currentURL = url;
|
||||
init();
|
||||
}
|
||||
|
||||
function closeWindow(messageId, additionalMessageId) {
|
||||
if (!matcherWindow) return;
|
||||
if (matcherFrame) {
|
||||
matcherFrame.remove(); matcherFrame = null;
|
||||
} else {
|
||||
matcherWindow.close();
|
||||
}
|
||||
unload();
|
||||
}
|
||||
|
||||
matchService.match = function(contactId) {
|
||||
var url =
|
||||
'/contacts/views/matching/matching_contacts.html?contactId=' +
|
||||
contactId;
|
||||
openWindow(url);
|
||||
};
|
||||
|
||||
matchService.showDuplicateContacts = function() {
|
||||
var url =
|
||||
'/contacts/views/matching/matching_contacts.html';
|
||||
openWindow(url);
|
||||
};
|
||||
|
||||
function init(){
|
||||
var target = matcherFrame || matcherWindow;
|
||||
target.addEventListener('load', function fn(){
|
||||
target.removeEventListener('load', fn);
|
||||
if (matcherFrame) matcherWindow = matcherFrame.contentWindow;
|
||||
matcherWindow.postMessage({
|
||||
type: 'sync'
|
||||
}, CONTACTS_APP_ORIGIN);
|
||||
if (!matcherFrame) matcherWindow.onunload = function(){
|
||||
window.postMessage({
|
||||
type: 'window_close'
|
||||
}, CONTACTS_APP_ORIGIN);
|
||||
};
|
||||
}, false);
|
||||
}
|
||||
|
||||
// This function can also be executed when other messages arrive
|
||||
// That's why we cannot call notifySettings outside the switch block
|
||||
function messageHandler(e) {
|
||||
if (!currentURL || e.origin !== CONTACTS_APP_ORIGIN) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data = e.data;
|
||||
|
||||
switch (data.type) {
|
||||
case 'ready':
|
||||
matcherWindow.postMessage({
|
||||
type: 'dom_transition_end',
|
||||
data: ''
|
||||
}, CONTACTS_APP_ORIGIN);
|
||||
window.dispatchEvent(new CustomEvent('image-loader-pause'));
|
||||
break;
|
||||
case 'window_close':
|
||||
closeWindow(data.messageId, data.additionalMessageId);
|
||||
break;
|
||||
case 'show_duplicate_contacts':
|
||||
matcherWindow.postMessage(data, CONTACTS_APP_ORIGIN);
|
||||
break;
|
||||
case 'duplicate_contacts_merged':
|
||||
matcherWindow.postMessage(data, CONTACTS_APP_ORIGIN);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function unload() {
|
||||
// Attaching again scrolling handlers on the contact list's image loader
|
||||
matcherWindow = currentURL = null;
|
||||
window.dispatchEvent(new CustomEvent('image-loader-resume'));
|
||||
}
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
|
||||
exports.MatchService = matchService;
|
||||
|
||||
}(window));
|
||||
49
gaia/apps/communications/contacts/js/migrator.js
Executable file
49
gaia/apps/communications/contacts/js/migrator.js
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
'use strict';
|
||||
|
||||
/* global fb, utils, LazyLoader, DatastoreMigration, ImportStatusData*/
|
||||
/* exported Migrator */
|
||||
|
||||
var Migrator = (function(){
|
||||
var start = function start(config) {
|
||||
if (!config || !config.fbMigrated) {
|
||||
migrateFbContacts();
|
||||
}
|
||||
|
||||
if (!config || !config.accessTokenMigrated) {
|
||||
migrateFbToken();
|
||||
}
|
||||
};
|
||||
|
||||
function migrateFbContacts() {
|
||||
LazyLoader.load('js/fb/datastore_migrator.js', function() {
|
||||
new DatastoreMigration().start();
|
||||
});
|
||||
}
|
||||
|
||||
function migrateFbToken() {
|
||||
LazyLoader.load([
|
||||
'/shared/js/contacts/import/import_status_data.js',
|
||||
], function() {
|
||||
var STORAGE_KEY = fb.utils.TOKEN_DATA_KEY;
|
||||
|
||||
ImportStatusData.get(STORAGE_KEY).then(function(datastoreToken) {
|
||||
if (!datastoreToken) {
|
||||
utils.cookie.update({accessTokenMigrated: true});
|
||||
window.asyncStorage.getItem(STORAGE_KEY, function(asyncstorageToken) {
|
||||
if (asyncstorageToken) {
|
||||
ImportStatusData.put(STORAGE_KEY, asyncstorageToken).then(
|
||||
function() {
|
||||
window.asyncStorage.removeItem(STORAGE_KEY);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
start: start
|
||||
};
|
||||
})();
|
||||
290
gaia/apps/communications/contacts/js/navigation.js
Executable file
290
gaia/apps/communications/contacts/js/navigation.js
Executable file
|
|
@ -0,0 +1,290 @@
|
|||
'use strict';
|
||||
/* global LazyLoader */
|
||||
/* exported navigationStack */
|
||||
|
||||
function navigationStack(currentView) {
|
||||
/* jshint validthis:true */
|
||||
// Each transition entry includes a 'forwards' property including the
|
||||
// classes which will be added to the 'current' and 'next' view when the
|
||||
// transition goes forwards, as well as a 'backwards' property including the
|
||||
// classes which will be added to the 'current' and 'next' view when the
|
||||
// transition goes backwards.
|
||||
this.transitions = {
|
||||
'none': {
|
||||
forwards: {},
|
||||
backwards: {}
|
||||
},
|
||||
'right-left': {
|
||||
forwards: {
|
||||
next: 'app-go-in'
|
||||
},
|
||||
backwards: {
|
||||
current: 'app-go-back-out'
|
||||
}
|
||||
},
|
||||
'popup': {
|
||||
forwards: {
|
||||
next: 'app-go-up-in'
|
||||
},
|
||||
backwards: {
|
||||
current: 'app-go-up-back-out'
|
||||
}
|
||||
},
|
||||
'activity-popup': {
|
||||
forwards: {
|
||||
next: 'app-go-up-in'
|
||||
},
|
||||
backwards: {}
|
||||
},
|
||||
'fade-in': {
|
||||
forwards: {
|
||||
next: 'fade-in'
|
||||
},
|
||||
backwards: {
|
||||
current: 'fade-out'
|
||||
}
|
||||
},
|
||||
'go-deeper': {
|
||||
forwards: {
|
||||
current: 'app-go-deeper-out',
|
||||
next: 'app-go-deeper-in'
|
||||
},
|
||||
backwards: {
|
||||
current: 'app-go-deeper-back-out',
|
||||
next: 'app-go-deeper-back-in'
|
||||
}
|
||||
},
|
||||
'go-deeper-search': {
|
||||
forwards: {
|
||||
current: 'move-out-from-forward',
|
||||
next: 'move-in-from-forward'
|
||||
},
|
||||
backwards: {
|
||||
current: 'move-out-from-backward',
|
||||
next: 'move-in-from-backward'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var COMMS_APP_ORIGIN = location.origin;
|
||||
var screenshotViewId = 'view-screenshot';
|
||||
var _currentView = currentView;
|
||||
document.getElementById(currentView).classList.add('current');
|
||||
this.stack = [];
|
||||
|
||||
navigationStack._zIndex = navigationStack._zIndex || 0;
|
||||
|
||||
this.stack.push({view: _currentView, transition: 'popup',
|
||||
zIndex: ++navigationStack._zIndex});
|
||||
|
||||
var waitForAnimation = function ng_waitForAnimation(view, callback) {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.addEventListener('animationend', function ng_onAnimationEnd() {
|
||||
view.removeEventListener('animationend', ng_onAnimationEnd);
|
||||
setTimeout(callback, 0);
|
||||
});
|
||||
};
|
||||
|
||||
this.go = function go(nextView, transition, callback) {
|
||||
if (_currentView === nextView) {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var parent = window.parent;
|
||||
if (nextView == 'view-contact-form') {
|
||||
parent.postMessage({type: 'hide-navbar'}, COMMS_APP_ORIGIN);
|
||||
}
|
||||
|
||||
// Remove items that match nextView from the stack to prevent duplicates.
|
||||
this.stack = this.stack.filter(function(item) {
|
||||
return item.view != nextView;
|
||||
});
|
||||
|
||||
var current;
|
||||
var currentClassList;
|
||||
// Performance is very bad when there are too many contacts so we use
|
||||
// -moz-element and animate this 'screenshot" element.
|
||||
if (transition.indexOf('go-deeper') === 0) {
|
||||
current = document.getElementById(screenshotViewId);
|
||||
|
||||
// Load the screenshot dom content
|
||||
LazyLoader.load([current]);
|
||||
|
||||
current.style.zIndex = this.stack[this.stack.length - 1].zIndex;
|
||||
currentClassList = current.classList;
|
||||
if (transition.indexOf('search') !== -1) {
|
||||
currentClassList.add('search');
|
||||
} else {
|
||||
currentClassList.add('contact-list');
|
||||
}
|
||||
currentClassList.remove('hide');
|
||||
} else {
|
||||
current = document.getElementById(_currentView);
|
||||
currentClassList = current.classList;
|
||||
}
|
||||
|
||||
var forwardsClasses = this.transitions[transition].forwards;
|
||||
|
||||
// Add forwards class to current view.
|
||||
currentClassList.add('block-item');
|
||||
if (forwardsClasses.current) {
|
||||
currentClassList.add(forwardsClasses.current);
|
||||
}
|
||||
|
||||
var next = document.getElementById(nextView);
|
||||
// Add forwards class to next view.
|
||||
if (forwardsClasses.next) {
|
||||
next.classList.add(forwardsClasses.next);
|
||||
next.addEventListener('animationend', function ng_onNextBackwards(ev) {
|
||||
next.removeEventListener('animationend', ng_onNextBackwards);
|
||||
next.classList.remove('block-item');
|
||||
});
|
||||
}
|
||||
|
||||
next.classList.add('current');
|
||||
var realCurrentView = document.getElementById(_currentView);
|
||||
var _callbackInner = function _callback() {
|
||||
next.classList.add('current');
|
||||
realCurrentView.classList.remove('current');
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (transition === 'none') {
|
||||
setTimeout(_callbackInner, 0);
|
||||
} else {
|
||||
waitForAnimation(next, _callbackInner);
|
||||
}
|
||||
|
||||
var zIndex = ++navigationStack._zIndex;
|
||||
this.stack.push({ view: nextView, transition: transition,
|
||||
zIndex: zIndex});
|
||||
next.style.zIndex = zIndex;
|
||||
_currentView = nextView;
|
||||
};
|
||||
|
||||
this.back = function back(callback) {
|
||||
if (this.stack.length < 2) {
|
||||
if (typeof callback === 'function') {
|
||||
setTimeout(callback, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var currentView = this.stack.pop();
|
||||
var current = document.getElementById(currentView.view);
|
||||
var currentClassList = current.classList;
|
||||
|
||||
var nextView = this.stack[this.stack.length - 1];
|
||||
var transition = currentView.transition;
|
||||
|
||||
var next = document.getElementById(nextView.view);
|
||||
var nextClassList;
|
||||
// Performance is very bad when there are too many contacts so we use
|
||||
// -moz-element and animate this 'screenshot" element.
|
||||
if (transition.indexOf('go-deeper') === 0) {
|
||||
next = document.getElementById(screenshotViewId);
|
||||
} else {
|
||||
next = document.getElementById(nextView.view);
|
||||
}
|
||||
nextClassList = next.classList;
|
||||
|
||||
var forwardsClasses = this.transitions[transition].forwards;
|
||||
var backwardsClasses = this.transitions[transition].backwards;
|
||||
|
||||
if (currentView.view == 'view-contact-form') {
|
||||
parent.postMessage({type: 'show-navbar'}, COMMS_APP_ORIGIN);
|
||||
}
|
||||
|
||||
// Add backwards class to current view.
|
||||
if (backwardsClasses.current) {
|
||||
currentClassList.add('block-item');
|
||||
currentClassList.add(backwardsClasses.current);
|
||||
current.addEventListener('animationend',
|
||||
function ng_onCurrentBackwards() {
|
||||
current.removeEventListener('animationend', ng_onCurrentBackwards);
|
||||
// Once the backwards animation completes, delete the added classes
|
||||
// to restore the elements to their initial state.
|
||||
currentClassList.remove(forwardsClasses.next);
|
||||
currentClassList.remove(backwardsClasses.current);
|
||||
current.style.zIndex = null;
|
||||
currentClassList.remove('block-item');
|
||||
if (!backwardsClasses.next) {
|
||||
nextClassList.remove('block-item');
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
current.style.zIndex = null;
|
||||
currentClassList.remove('block-item');
|
||||
if (!backwardsClasses.next) {
|
||||
nextClassList.remove('block-item');
|
||||
}
|
||||
}
|
||||
|
||||
next.style.zIndex = nextView.zIndex;
|
||||
|
||||
// Add backwards class to next view.
|
||||
if (backwardsClasses.next) {
|
||||
nextClassList.add(backwardsClasses.next);
|
||||
next.addEventListener('animationend', function ng_onNextBackwards() {
|
||||
next.removeEventListener('animationend', ng_onNextBackwards);
|
||||
// Once the backwards animation completes, delete the added classes
|
||||
// to restore the elements to their initial state.
|
||||
nextClassList.remove(forwardsClasses.current);
|
||||
nextClassList.remove(backwardsClasses.next);
|
||||
if (transition.indexOf('go-deeper') === 0) {
|
||||
// Hide the -moz-element element once the animation completes.
|
||||
nextClassList.add('hide');
|
||||
nextClassList.remove('search');
|
||||
nextClassList.remove('contact-list');
|
||||
}
|
||||
nextClassList.remove('block-item');
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById(nextView.view).classList.add('current');
|
||||
var _callbackInner = function _callbackInner() {
|
||||
document.getElementById(nextView.view).classList.add('current');
|
||||
currentClassList.remove('current');
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if ((!backwardsClasses.current && !backwardsClasses.next) ||
|
||||
transition === 'none') {
|
||||
setTimeout(_callbackInner, 0);
|
||||
} else {
|
||||
waitForAnimation(current, _callbackInner);
|
||||
}
|
||||
_currentView = nextView.view;
|
||||
navigationStack._zIndex = nextView.zIndex;
|
||||
};
|
||||
|
||||
this.home = function home(callback) {
|
||||
if (this.stack.length < 2) {
|
||||
if (typeof callback === 'function') {
|
||||
setTimeout(callback, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
while (this.stack.length > 1) {
|
||||
this.back(callback);
|
||||
}
|
||||
};
|
||||
|
||||
this.currentView = function currentView() {
|
||||
return _currentView != null ? _currentView : '';
|
||||
};
|
||||
}
|
||||
94
gaia/apps/communications/contacts/js/nfc.js
Executable file
94
gaia/apps/communications/contacts/js/nfc.js
Executable file
|
|
@ -0,0 +1,94 @@
|
|||
'use strict';
|
||||
|
||||
/* global LazyLoader, ContactToVcard, MozNDEFRecord, fb, utils,
|
||||
NDEF, NfcUtils, utils*/
|
||||
/* exported NFC */
|
||||
|
||||
(function(exports) {
|
||||
var mozNfc = window.navigator.mozNfc;
|
||||
var currentContact;
|
||||
var vCardContact;
|
||||
var mozNfcPeer;
|
||||
|
||||
var startListening = function(contact) {
|
||||
if (!mozNfc) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentContact = contact;
|
||||
// We cannot share Facebook data via NFC so we check if the contact
|
||||
// is an FB contacts. However, if the contact is linked to a regular
|
||||
// mozContact, we can share linked data
|
||||
if (fb && !(fb.isFbContact(contact) && !fb.isFbLinked(contact))) {
|
||||
mozNfc.onpeerready = handlePeerReady;
|
||||
} else {
|
||||
mozNfc.onpeerready = handlePeerReadyForFb;
|
||||
}
|
||||
};
|
||||
|
||||
var stopListening = function() {
|
||||
if (!mozNfc) {
|
||||
return;
|
||||
}
|
||||
|
||||
mozNfc.onpeerready = null;
|
||||
currentContact = null;
|
||||
vCardContact = null;
|
||||
mozNfcPeer = null;
|
||||
};
|
||||
|
||||
var handlePeerReady = function(event) {
|
||||
mozNfcPeer = event.peer;
|
||||
LazyLoader.load([
|
||||
'/shared/js/contact2vcard.js',
|
||||
'/shared/js/setImmediate.js',
|
||||
'/shared/js/nfc_utils.js'
|
||||
], function() {
|
||||
ContactToVcard(
|
||||
[currentContact],
|
||||
function append(vcard) {
|
||||
vCardContact = vcard;
|
||||
},
|
||||
function success() {
|
||||
sendContact();
|
||||
},
|
||||
// use default batch size
|
||||
null,
|
||||
// We don't want to share a profile photo via NFC,
|
||||
// like on Android:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1003767#c5
|
||||
true
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
var sendContact = function() {
|
||||
var nfcUtils = new NfcUtils();
|
||||
var NDEFRecord = new MozNDEFRecord({
|
||||
tnf: NDEF.TNF_MIME_MEDIA,
|
||||
type: nfcUtils.fromUTF8('text/vcard'),
|
||||
payload: nfcUtils.fromUTF8(vCardContact)
|
||||
});
|
||||
|
||||
var promise = mozNfcPeer.sendNDEF([NDEFRecord]);
|
||||
promise.then(() => {
|
||||
console.log('Contact succesfuly sent');
|
||||
}).catch(e => {
|
||||
console.log('Something goes wrong : ' + e);
|
||||
});
|
||||
};
|
||||
|
||||
var handlePeerReadyForFb = function() {
|
||||
utils.status.show({
|
||||
id: 'facebook-export-forbidden'
|
||||
});
|
||||
};
|
||||
|
||||
var nfc_tools = {
|
||||
startListening: startListening,
|
||||
stopListening: stopListening
|
||||
};
|
||||
|
||||
exports.NFC = nfc_tools;
|
||||
|
||||
}(window));
|
||||
185
gaia/apps/communications/contacts/js/service_extensions.js
Executable file
185
gaia/apps/communications/contacts/js/service_extensions.js
Executable file
|
|
@ -0,0 +1,185 @@
|
|||
'use strict';
|
||||
/* exported extServices*/
|
||||
/* global fb */
|
||||
/* global utils */
|
||||
|
||||
(function(exports) {
|
||||
var onContactsImportedCb = null;
|
||||
var extServices = {
|
||||
set onContactsImported(cb) {
|
||||
if (typeof cb !== 'function') {
|
||||
return;
|
||||
}
|
||||
onContactsImportedCb = cb;
|
||||
}
|
||||
};
|
||||
|
||||
var extensionFrame = document.querySelector('#iframe_extensions');
|
||||
var oauthFrame = document.querySelector('#iframe_oauth');
|
||||
oauthFrame.src = '/shared/pages/import/oauth.html';
|
||||
var currentURI, access_token;
|
||||
var canClose = true, canCloseLogout = true;
|
||||
var closeRequested = false;
|
||||
|
||||
extServices.importGmail = function(evt) {
|
||||
loadService('gmail');
|
||||
};
|
||||
|
||||
extServices.importLive = function(evt) {
|
||||
loadService('live');
|
||||
};
|
||||
|
||||
function loadService(serviceName) {
|
||||
closeRequested = false;
|
||||
canClose = false;
|
||||
canCloseLogout = false;
|
||||
load('/shared/pages/import/import.html?service=' + serviceName,
|
||||
'friends', serviceName);
|
||||
}
|
||||
|
||||
function load(uri, from, serviceName) {
|
||||
oauthFrame.contentWindow.postMessage({
|
||||
type: 'start',
|
||||
data: {
|
||||
from: from,
|
||||
service: serviceName
|
||||
}
|
||||
}, fb.CONTACTS_APP_ORIGIN);
|
||||
currentURI = uri;
|
||||
}
|
||||
|
||||
function unload() {
|
||||
// Attaching again scrolling handlers on the contact list's image loader
|
||||
window.dispatchEvent(new CustomEvent('image-loader-resume'));
|
||||
extensionFrame.src = currentURI = null;
|
||||
}
|
||||
|
||||
function close(messageId, additionalMessageId) {
|
||||
// Aborting the close if was closed previously, ie: no contacts
|
||||
if (!extensionFrame.classList.contains('opening')) {
|
||||
return;
|
||||
}
|
||||
extensionFrame.addEventListener('transitionend', function tclose() {
|
||||
extensionFrame.removeEventListener('transitionend', tclose);
|
||||
extensionFrame.classList.add('hidden');
|
||||
if (canClose === true && canCloseLogout === true) {
|
||||
unload();
|
||||
}
|
||||
else {
|
||||
closeRequested = true;
|
||||
}
|
||||
|
||||
if (messageId) {
|
||||
utils.status.show(messageId, additionalMessageId);
|
||||
}
|
||||
// Otherwise we do nothing as the sync process will finish sooner or later
|
||||
});
|
||||
extensionFrame.classList.remove('opening');
|
||||
}
|
||||
|
||||
function notifySettings(evtype) {
|
||||
// Notify observers that a change from FB could have happened
|
||||
var eventType = evtype || 'fb_changed';
|
||||
|
||||
var event = new CustomEvent(eventType, {
|
||||
'detail' : true
|
||||
});
|
||||
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// This function can also be executed when other messages arrive
|
||||
// That's why we cannot call notifySettings outside the switch block
|
||||
function messageHandler(e) {
|
||||
if (!currentURI || e.origin !== fb.CONTACTS_APP_ORIGIN) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data = e.data;
|
||||
|
||||
switch (data.type) {
|
||||
case 'ready':
|
||||
extensionFrame.classList.remove('hidden');
|
||||
window.setTimeout(function displaying() {
|
||||
extensionFrame.classList.add('opening');
|
||||
extensionFrame.addEventListener('transitionend', function topen() {
|
||||
extensionFrame.removeEventListener('transitionend', topen);
|
||||
extensionFrame.contentWindow.postMessage({
|
||||
type: 'dom_transition_end',
|
||||
data: ''
|
||||
}, fb.CONTACTS_APP_ORIGIN);
|
||||
// Stop scrolling listeners on the contact list's image loader to
|
||||
// prevent images cancelled while friends are being imported
|
||||
window.dispatchEvent(new CustomEvent('image-loader-pause'));
|
||||
});
|
||||
}, 0);
|
||||
break;
|
||||
|
||||
case 'authenticated':
|
||||
extensionFrame.src = currentURI;
|
||||
access_token = data.data;
|
||||
break;
|
||||
|
||||
case 'token_stored':
|
||||
notifySettings('fb_token_ready');
|
||||
break;
|
||||
|
||||
case 'token_error':
|
||||
notifySettings('fb_token_error');
|
||||
break;
|
||||
|
||||
case 'abort':
|
||||
unload();
|
||||
break;
|
||||
|
||||
case 'window_close':
|
||||
close(data.messageId, data.additionalMessageId);
|
||||
notifySettings();
|
||||
break;
|
||||
|
||||
case 'import_updated':
|
||||
extensionFrame.contentWindow.postMessage({
|
||||
type: 'contacts_loaded',
|
||||
data: ''
|
||||
}, fb.CONTACTS_APP_ORIGIN);
|
||||
if (onContactsImportedCb) {
|
||||
onContactsImportedCb();
|
||||
onContactsImportedCb = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sync_finished':
|
||||
// Sync finished thus the iframe can be safely "removed"
|
||||
canClose = true;
|
||||
if (closeRequested && canCloseLogout) {
|
||||
unload();
|
||||
}
|
||||
|
||||
// Check whether there has been changes or not
|
||||
if (data.data > 0) {
|
||||
notifySettings();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'logout_finished':
|
||||
canCloseLogout = true;
|
||||
if (closeRequested && canClose) {
|
||||
unload();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'messaging_ready':
|
||||
extensionFrame.contentWindow.postMessage({
|
||||
type: 'token',
|
||||
data: access_token
|
||||
}, fb.CONTACTS_APP_ORIGIN);
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
|
||||
exports.ExtServices = extServices;
|
||||
|
||||
}(window));
|
||||
31
gaia/apps/communications/contacts/js/tag_options.js
Executable file
31
gaia/apps/communications/contacts/js/tag_options.js
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
'use strict';
|
||||
/* exported TAG_OPTIONS */
|
||||
var _ = navigator.mozL10n.get;
|
||||
|
||||
var TAG_OPTIONS = {
|
||||
'phone-type' : [
|
||||
{type: 'mobile', value: _('mobile')},
|
||||
{type: 'home', value: _('home')},
|
||||
{type: 'work', value: _('work')},
|
||||
{type: 'personal', value: _('personal')},
|
||||
{type: 'faxHome', value: _('faxHome')},
|
||||
{type: 'faxOffice', value: _('faxOffice')},
|
||||
{type: 'faxOther', value: _('faxOther')},
|
||||
{type: 'other', value: _('other')}
|
||||
],
|
||||
'email-type' : [
|
||||
{type: 'personal', value: _('personal')},
|
||||
{type: 'home', value: _('home')},
|
||||
{type: 'work', value: _('work')},
|
||||
{type: 'other', value: _('other')}
|
||||
],
|
||||
'address-type' : [
|
||||
{type: 'current', value: _('current')},
|
||||
{type: 'home', value: _('home')},
|
||||
{type: 'work', value: _('work')}
|
||||
],
|
||||
'date-type': [
|
||||
{type: 'birthday', value: _('birthday')},
|
||||
{type: 'anniversary', value: _('anniversary')}
|
||||
]
|
||||
};
|
||||
123
gaia/apps/communications/contacts/js/utilities/cookie.js
Executable file
123
gaia/apps/communications/contacts/js/utilities/cookie.js
Executable file
|
|
@ -0,0 +1,123 @@
|
|||
'use strict';
|
||||
|
||||
//
|
||||
// Utility API for manipulating config stored in local cookie.
|
||||
//
|
||||
(function() {
|
||||
var utils = window.utils = window.utils || {};
|
||||
utils.cookie = {};
|
||||
|
||||
// Bump this number if the format of the cookie changes
|
||||
var COOKIE_VERSION = 6;
|
||||
|
||||
// Default values for items stored in the configuration
|
||||
var COOKIE_DEFAULTS = {
|
||||
order: false,
|
||||
viewHeight: -1,
|
||||
rowsPerPage: -1,
|
||||
fbMigrated: false,
|
||||
fbScheduleDone: false,
|
||||
defaultImage: true,
|
||||
accessTokenMigrated: false,
|
||||
fbCleaningInProgress: 0
|
||||
};
|
||||
|
||||
// Only allow these properties to be stored in the config
|
||||
var COOKIE_PROPS = Object.keys(COOKIE_DEFAULTS);
|
||||
|
||||
// Set as a constant in the far future to avoid calling new Date
|
||||
var EXPIRATION_DATE = 'Fri, 31 Dec 9999 23:59:59 GMT';
|
||||
var COOKIE_NAME = 'preferences';
|
||||
var COOKIE_NAME_EQ = COOKIE_NAME + '=';
|
||||
var COOKIE_NAME_LENGHT = COOKIE_NAME_EQ.length;
|
||||
|
||||
// Parses the cookie returning the corresponding object
|
||||
// Returns null if the cookie string does not conform with expected format
|
||||
function _parseCookie(cookie) {
|
||||
// Expecting "preferences=<JSON value>"
|
||||
var index = cookie.indexOf(COOKIE_NAME_EQ);
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
var cookieVal = cookie.substring(index + COOKIE_NAME_LENGHT);
|
||||
if (!cookieVal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Setup default value for cookie with version that will trigger update
|
||||
var result = {
|
||||
version: -1
|
||||
};
|
||||
try {
|
||||
result = JSON.parse(decodeURIComponent(cookieVal));
|
||||
} catch (error) {
|
||||
console.warn('Could not parse current cookie, rebuilding it');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Load and return the cookie config if present. Returns null if the
|
||||
// cookie is missing, has an incorrect format or versions mismatch.
|
||||
utils.cookie.load = function() {
|
||||
if (!document.cookie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var cookie = _parseCookie(document.cookie);
|
||||
|
||||
// If the cookie is out-of-date, then we need to update it
|
||||
// and re-parse since the format might have changed.
|
||||
if (cookie && cookie.version !== COOKIE_VERSION) {
|
||||
_updateCookie(cookie);
|
||||
cookie = _parseCookie(document.cookie);
|
||||
}
|
||||
|
||||
return cookie;
|
||||
};
|
||||
|
||||
// Update the current cookie config with the given set of configuration
|
||||
// properties. The passed in configuration properties can be a subset of
|
||||
// the values stored in the cookie. Unspecified values will be unchanged.
|
||||
utils.cookie.update = function(cfg) {
|
||||
_updateCookie(utils.cookie.load(), cfg);
|
||||
};
|
||||
|
||||
// Internal helper function that writes out a new cookie based on a set
|
||||
// of existing value, a set of new values, and defaults.
|
||||
function _updateCookie(oldCookie, cfg) {
|
||||
oldCookie = oldCookie || {};
|
||||
cfg = cfg || {};
|
||||
|
||||
var newCookie = {version: COOKIE_VERSION};
|
||||
|
||||
// For each allowed property in the config, pick the right value given
|
||||
// our input. This intentionally removes old properties in the cookie
|
||||
// that may no longer be in our allowed COOKIE_PROPS.
|
||||
for (var i = 0, n = COOKIE_PROPS.length; i < n; ++i) {
|
||||
var prop = COOKIE_PROPS[i];
|
||||
// Prefer configuration overrides provided
|
||||
if (prop in cfg) {
|
||||
newCookie[prop] = cfg[prop];
|
||||
// Otherwise use previously set values
|
||||
} else if (prop in oldCookie) {
|
||||
newCookie[prop] = oldCookie[prop];
|
||||
// Finally, fall back to defaults
|
||||
} else {
|
||||
newCookie[prop] = COOKIE_DEFAULTS[prop];
|
||||
}
|
||||
}
|
||||
|
||||
document.cookie = COOKIE_NAME + '=' +
|
||||
encodeURIComponent(JSON.stringify(newCookie)) +
|
||||
';expires=' + EXPIRATION_DATE;
|
||||
|
||||
}
|
||||
|
||||
utils.cookie.getDefault = function(prop) {
|
||||
return COOKIE_DEFAULTS[prop];
|
||||
};
|
||||
|
||||
utils.cookie.COOKIE_DEFAULTS = COOKIE_DEFAULTS;
|
||||
utils.cookie.COOKIE_VERSION = COOKIE_VERSION;
|
||||
})();
|
||||
18
gaia/apps/communications/contacts/js/utilities/extract_params.js
Executable file
18
gaia/apps/communications/contacts/js/utilities/extract_params.js
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
'use strict';
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
if (!utils.extractParams) {
|
||||
utils.extractParams = function extractParams(url) {
|
||||
if (!url) {
|
||||
return -1;
|
||||
}
|
||||
var ret = {};
|
||||
var params = url.split('&');
|
||||
for (var i = 0; i < params.length; i++) {
|
||||
var currentParam = params[i].split('=');
|
||||
ret[currentParam[0]] = currentParam[1];
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
95
gaia/apps/communications/contacts/js/utilities/icc_handler.js
Executable file
95
gaia/apps/communications/contacts/js/utilities/icc_handler.js
Executable file
|
|
@ -0,0 +1,95 @@
|
|||
/* exported IccHandler */
|
||||
//
|
||||
// IccManager Handler, abstract the new IccManager Multi SIM
|
||||
// API.
|
||||
// Generates code and handles listener for multiple SIMs
|
||||
//
|
||||
'use strict';
|
||||
|
||||
var IccHandler = function IccHandler() {
|
||||
|
||||
var iccManager = null;
|
||||
var isSingleSIMApi = false;
|
||||
var iccs = {};
|
||||
var sims = [];
|
||||
var mobileConnections = null;
|
||||
|
||||
var init = function init(domGenerator, cb) {
|
||||
iccManager = navigator.mozIccManager;
|
||||
// Avoid any icc operation in devices that don't
|
||||
// have icc (desktop, tablets, etc.)
|
||||
if (!iccManager) {
|
||||
if (typeof cb === 'function') {
|
||||
cb();
|
||||
}
|
||||
return;
|
||||
}
|
||||
mobileConnections = navigator.mozMobileConnections || [];
|
||||
isSingleSIMApi = typeof iccManager.getIccById !== 'function';
|
||||
|
||||
if (isSingleSIMApi) {
|
||||
iccs['default'] = iccManager;
|
||||
} else {
|
||||
iccManager.iccIds.forEach(function(iccId) {
|
||||
iccs[iccId] = iccManager.getIccById(iccId);
|
||||
});
|
||||
for (var i = 0; i < mobileConnections.length; i++) {
|
||||
sims[i] = iccs[mobileConnections[i].iccId] || null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
domGenerator.setIccList(sims);
|
||||
domGenerator.generateDOM();
|
||||
|
||||
subscribeToChanges(cb);
|
||||
};
|
||||
|
||||
// Make all the SIMS to listen to the same change observer
|
||||
var subscribeToChanges = function subscribeToChanges(cb) {
|
||||
if (!navigator.mozIccManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(iccs).forEach(function onIccId(iccId) {
|
||||
var icc = iccs[iccId];
|
||||
if (icc !== null) {
|
||||
icc.oncardstatechange = cb;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Get the status of all the SIMs, not just the status, also
|
||||
// the proper icc object
|
||||
var getStatus = function getStatus() {
|
||||
var status = [];
|
||||
Object.keys(iccs).forEach(function onIccId(iccId) {
|
||||
var icc = iccs[iccId];
|
||||
status.push({
|
||||
'iccId': iccId,
|
||||
'icc': icc,
|
||||
'cardState': icc.cardState
|
||||
});
|
||||
});
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
// Returns the specific icc depending on id.
|
||||
// If id is null will try to get the one named 'default'
|
||||
var getIccById = function getIccById(id) {
|
||||
if (!id) {
|
||||
id = 'default';
|
||||
}
|
||||
|
||||
return iccs[id];
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
'getStatus': getStatus,
|
||||
'getIccById': getIccById,
|
||||
'init': init
|
||||
};
|
||||
|
||||
}();
|
||||
272
gaia/apps/communications/contacts/js/utilities/ice_data.js
Executable file
272
gaia/apps/communications/contacts/js/utilities/ice_data.js
Executable file
|
|
@ -0,0 +1,272 @@
|
|||
'use strict';
|
||||
|
||||
/* global Cache */
|
||||
/* global ICEStore */
|
||||
|
||||
/**
|
||||
* Module to handle ICE data across different parts of the application
|
||||
* This module just know about the internal representation and how
|
||||
* to deal with share ICE information (which has different format).
|
||||
*/
|
||||
|
||||
(function ICEData(exports) {
|
||||
|
||||
if (exports.ICEData) {
|
||||
return;
|
||||
}
|
||||
|
||||
var localIceContacts = [];
|
||||
var ICE_CONTACTS_KEY = 'ice-contacts';
|
||||
var onChangeCallbacks = [];
|
||||
var onChangeAttached = false;
|
||||
var pendingChanges = [];
|
||||
var performingChange = false;
|
||||
|
||||
/**
|
||||
* Loads the local formated data into an internal
|
||||
* representation.
|
||||
* @return (Promise) fullfill when the load finish.
|
||||
*/
|
||||
function load() {
|
||||
var iceContactsIds = [
|
||||
{
|
||||
id: undefined,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
id: undefined,
|
||||
active: false
|
||||
}
|
||||
];
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
window.asyncStorage.getItem(ICE_CONTACTS_KEY, function(data) {
|
||||
if (data) {
|
||||
if (data[0]) {
|
||||
iceContactsIds[0] = data[0];
|
||||
}
|
||||
if (data[1]) {
|
||||
iceContactsIds[1] = data[1];
|
||||
}
|
||||
}
|
||||
localIceContacts = iceContactsIds;
|
||||
resolve(localIceContacts);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of an ICE contact, both in local and in the
|
||||
* datastore
|
||||
* @param id (string) contact id
|
||||
* @param pos (int) current position (0,1)
|
||||
* @param active (boolean) ice contact is active or not
|
||||
*/
|
||||
function setICEContact(id, pos, active) {
|
||||
active = active || false;
|
||||
|
||||
Cache.evict();
|
||||
|
||||
// Save locally
|
||||
localIceContacts[pos] = {
|
||||
id: id,
|
||||
active: active
|
||||
};
|
||||
return setIceContactsItem(localIceContacts).then(() => {
|
||||
// Save in the datastore
|
||||
return modifyICEInDS();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values for ICE contacts, both in local and in the
|
||||
* datastore
|
||||
* @param iceContactList the list with ids of ICE Contacts to be set
|
||||
*/
|
||||
function setICEContacts(iceContactList) {
|
||||
if (!Array.isArray(iceContactList)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Save locally
|
||||
localIceContacts = [
|
||||
{
|
||||
id: iceContactList[0],
|
||||
active: !!iceContactList[0]
|
||||
},
|
||||
{
|
||||
id: iceContactList[1],
|
||||
active: !!iceContactList
|
||||
}
|
||||
];
|
||||
|
||||
return setIceContactsItem(localIceContacts).then(() => {
|
||||
// Save in the datastore
|
||||
return modifyICEInDS();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the current local ice contacts into the datastore,
|
||||
* considering the active flag.
|
||||
* @return (Promise) fullfill when data saved in DS
|
||||
*/
|
||||
function modifyICEInDS() {
|
||||
var contacts = [];
|
||||
localIceContacts.forEach(function(iceContact) {
|
||||
if (iceContact.id && iceContact.active) {
|
||||
contacts.push(iceContact.id);
|
||||
}
|
||||
});
|
||||
|
||||
return ICEStore.setContacts(contacts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a specific contact id removes it from the
|
||||
* ice list, if present or disabled
|
||||
* @param id (String) contact id
|
||||
* @return (Promise) fullfill when data modified in DS
|
||||
*/
|
||||
function removeICEContact(id) {
|
||||
var contact = localIceContacts.find(function(x) {
|
||||
return x.id === id;
|
||||
});
|
||||
|
||||
if (!contact) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var index = localIceContacts.indexOf(contact);
|
||||
localIceContacts[index].id = null;
|
||||
localIceContacts[index].active = false;
|
||||
|
||||
setIceContactsItem(localIceContacts).then(function() {
|
||||
return modifyICEInDS().then(() => {
|
||||
notifyCallbacks(localIceContacts);
|
||||
});
|
||||
}).then(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function setIceContactsItem(iceContactsList) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
window.asyncStorage.setItem(ICE_CONTACTS_KEY, iceContactsList,
|
||||
resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an observer to be called when an ice contact
|
||||
* changed. We can add multiple listeners, since we will
|
||||
* need to react in several parts of the app to this changes.
|
||||
* Also the listener removes a contact id in case we are performing
|
||||
* a delete. In case of modification we don't do anythying as we
|
||||
* store just ids.
|
||||
* Listener will be attached just once, but we can keep adding
|
||||
* callbacks.
|
||||
* @param fn (Function) callback to call when ice contact change
|
||||
*/
|
||||
function listenForChanges(fn) {
|
||||
return new Promise(function(resolve) {
|
||||
if (!onChangeAttached) {
|
||||
load().then(function() {
|
||||
document.addEventListener('contactChanged', onChangeEvent);
|
||||
onChangeAttached = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
onChangeCallbacks.push(fn);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function notifyCallbacks(data) {
|
||||
onChangeCallbacks.forEach(function(fn) {
|
||||
fn(data);
|
||||
});
|
||||
}
|
||||
|
||||
function handleChangeEvent(evt) {
|
||||
// Figure out if we got a change in a ICE contact
|
||||
var contact = localIceContacts.find(function(x) {
|
||||
return x.id === evt.detail.contactID;
|
||||
});
|
||||
|
||||
if (!contact) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// If it's a delete update all the storages
|
||||
if (evt.detail.reason === 'remove') {
|
||||
return removeICEContact(contact.id);
|
||||
} else {
|
||||
notifyCallbacks(localIceContacts);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeEvent(evt) {
|
||||
// We should refresh the iceContacts before determining if it is necessary
|
||||
// to notify or not
|
||||
pendingChanges.push(evt);
|
||||
if (!performingChange) {
|
||||
performChange();
|
||||
}
|
||||
}
|
||||
|
||||
// We perform any change sequentially to avoid any race condition while
|
||||
// saving to disc the ICE state.
|
||||
function performChange() {
|
||||
performingChange = true;
|
||||
var evt = pendingChanges.pop();
|
||||
|
||||
if (!evt) {
|
||||
performingChange = false;
|
||||
return;
|
||||
}
|
||||
|
||||
function callback() {
|
||||
handleChangeEvent(evt).then(performChange);
|
||||
}
|
||||
|
||||
load().then(callback, callback);
|
||||
}
|
||||
|
||||
function getActiveIceContacts() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
load().then(function() {
|
||||
var result = [];
|
||||
localIceContacts.forEach(function(aContact) {
|
||||
if (aContact.active && aContact.id) {
|
||||
result.push(aContact.id);
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function stopListenForChanges() {
|
||||
document.removeEventListener('contactChanged', onChangeEvent);
|
||||
onChangeCallbacks = [];
|
||||
onChangeAttached = false;
|
||||
}
|
||||
|
||||
exports.ICEData = {
|
||||
load: load,
|
||||
setICEContact: setICEContact,
|
||||
setICEContacts: setICEContacts,
|
||||
removeICEContact: removeICEContact,
|
||||
get iceContacts() { return localIceContacts; },
|
||||
getActiveIceContacts: getActiveIceContacts,
|
||||
listenForChanges: listenForChanges,
|
||||
stopListenForChanges: stopListenForChanges
|
||||
};
|
||||
|
||||
|
||||
})(window);
|
||||
149
gaia/apps/communications/contacts/js/utilities/merge_helper.js
Executable file
149
gaia/apps/communications/contacts/js/utilities/merge_helper.js
Executable file
|
|
@ -0,0 +1,149 @@
|
|||
'use strict';
|
||||
|
||||
/* exported MergeHelper */
|
||||
/* globals LazyLoader, ICEData, contacts, ContactsService */
|
||||
|
||||
/**
|
||||
*
|
||||
* Merge helper file. Allows to perform a merge while respecting
|
||||
* all the ICE Settings
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
var MergeHelper = (function() {
|
||||
var DEPENDENCIES = [
|
||||
'/shared/js/contacts/contacts_merger.js',
|
||||
'/shared/js/contacts/utilities/image_thumbnail.js',
|
||||
'/shared/js/contacts/utilities/ice_store.js',
|
||||
'/contacts/js/utilities/ice_data.js',
|
||||
'/shared/js/async_storage.js'
|
||||
];
|
||||
|
||||
var eventHandler;
|
||||
|
||||
// Set the new ICE Contacts being contactId the id of the contact which
|
||||
// is now the merged contact
|
||||
function setNewIceContacts(merged, original, contactId) {
|
||||
if (!merged) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// If we still don't know the contact id
|
||||
// (this happens when merge is done from 'add new contact')
|
||||
if (!contactId || contactId === 'undefined') {
|
||||
eventHandler = onContactAdded.bind(null, merged, original);
|
||||
ContactsService.addListener('contactchange', eventHandler);
|
||||
}
|
||||
|
||||
return ICEData.setICEContacts(merged);
|
||||
}
|
||||
|
||||
function onContactAdded(mergedIceContacts, originalIceContacts, event) {
|
||||
if (event.reason !== 'create') {
|
||||
return;
|
||||
}
|
||||
|
||||
ContactsService.removeListener('contactchange', eventHandler);
|
||||
|
||||
var id = event.contactID;
|
||||
|
||||
// Let's determine what ICE Contact is changing
|
||||
var mergedICEContact1 = false;
|
||||
if (originalIceContacts[0] && mergedIceContacts[0] !==
|
||||
originalIceContacts[0]) {
|
||||
mergedIceContacts[0] = id;
|
||||
mergedICEContact1 = true;
|
||||
}
|
||||
|
||||
// This condition avoids to set ICE Contact 2 to the same contact as
|
||||
// ICE Contact 1. This may occur if ICE Contact 1 and ICE Contact 2 both
|
||||
// match an incoming (manually added contact)
|
||||
if (originalIceContacts[1] &&
|
||||
mergedIceContacts[1] !== originalIceContacts[1] && !mergedICEContact1) {
|
||||
mergedIceContacts[1] = id;
|
||||
}
|
||||
|
||||
ICEData.setICEContacts(mergedIceContacts);
|
||||
}
|
||||
|
||||
function getMergedIceContacts(mergeList, contactId) {
|
||||
return new Promise(function(resolve) {
|
||||
ICEData.load().then(function() {
|
||||
var iceContacts = ICEData.iceContacts.map((contact) => {
|
||||
return contact.active ? contact.id : null;
|
||||
});
|
||||
|
||||
var mergeListWithIds = mergeList.map((matching) => {
|
||||
return matching.matchingContact.id;
|
||||
});
|
||||
|
||||
var mergedIceContacts;
|
||||
|
||||
var isThereMerge = false;
|
||||
mergedIceContacts = iceContacts.map((iceContact) => {
|
||||
if(iceContact && mergeListWithIds.indexOf(iceContact) !== -1) {
|
||||
isThereMerge = true;
|
||||
return contactId;
|
||||
}
|
||||
return iceContact;
|
||||
});
|
||||
|
||||
// If there is no merge null value is returned
|
||||
if (!isThereMerge) {
|
||||
resolve([null, iceContacts]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Here we filter out duplicates
|
||||
mergedIceContacts = mergedIceContacts.map((id, index) => {
|
||||
if(id && mergedIceContacts.indexOf(id) === index) {
|
||||
return id;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
resolve([mergedIceContacts, iceContacts]);
|
||||
|
||||
}, function(error) {
|
||||
console.error('Error while obtaining ICE Contacts: ', error);
|
||||
resolve([]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function merge(contact, mergeList) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
LazyLoader.load(DEPENDENCIES, function loaded() {
|
||||
getMergedIceContacts(mergeList, contact.id).then(
|
||||
function(result) {
|
||||
var mergedIceContacts = result[0], originalIceContacts = result[1];
|
||||
var callbacks = {
|
||||
success: resolve,
|
||||
error: function(e) {
|
||||
console.error('Failed merging duplicate contacts: ', e.name);
|
||||
// If there is an error in the merge then we just
|
||||
// keep the old ICE Contacts
|
||||
ICEData.setICEContacts(originalIceContacts);
|
||||
ContactsService.removeListener('contactchange', eventHandler);
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
|
||||
var mergerFunction = contacts.Merger.merge.bind(null,
|
||||
contact, mergeList, callbacks);
|
||||
// We set the new ICEContact as the merged one
|
||||
// As a result, the contactChange events associated to the
|
||||
// Contacts merged (which are deleted) will be ignored
|
||||
// This is safer than relying on events as
|
||||
// there could race conditions
|
||||
setNewIceContacts(mergedIceContacts, originalIceContacts,
|
||||
contact.id).then(mergerFunction, mergerFunction);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
'merge': merge
|
||||
};
|
||||
}());
|
||||
36
gaia/apps/communications/contacts/js/utilities/mozContact.js
Executable file
36
gaia/apps/communications/contacts/js/utilities/mozContact.js
Executable file
|
|
@ -0,0 +1,36 @@
|
|||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
exports = exports || {};
|
||||
|
||||
var mozContactUtils = {
|
||||
// Checks if an object fields are empty, by empty means
|
||||
// field is null and if it's an array it's length is 0
|
||||
haveEmptyFields: function (obj, fields) {
|
||||
if (obj === null ||
|
||||
typeof(obj) != 'object' ||
|
||||
!fields || !fields.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var attr;
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
attr = fields[i];
|
||||
if (obj[attr]) {
|
||||
if (Array.isArray(obj[attr])) {
|
||||
if (obj[attr].length > 0) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
exports.mozContact = mozContactUtils;
|
||||
|
||||
})(window.utils);
|
||||
68
gaia/apps/communications/contacts/js/utilities/normalizer.js
Executable file
68
gaia/apps/communications/contacts/js/utilities/normalizer.js
Executable file
|
|
@ -0,0 +1,68 @@
|
|||
'use strict';
|
||||
|
||||
var utils = window.utils || {};
|
||||
|
||||
if (!utils.time) {
|
||||
(function() {
|
||||
utils.time = {};
|
||||
|
||||
var _ = navigator.mozL10n.get;
|
||||
|
||||
utils.time.pretty = function(time, now) {
|
||||
// 'now' attribute for test purposes.
|
||||
var prettyDate = '';
|
||||
|
||||
if (!time) {
|
||||
return prettyDate;
|
||||
}
|
||||
|
||||
function roundToDay(date) {
|
||||
var rounded = new Date(date);
|
||||
rounded.setHours(0);
|
||||
rounded.setMinutes(0);
|
||||
rounded.setSeconds(0);
|
||||
rounded.setMilliseconds(0);
|
||||
return rounded;
|
||||
}
|
||||
|
||||
now = now || Date.now();
|
||||
var day_diff = (roundToDay(now) - roundToDay(time)) / 86400000;
|
||||
if (!isNaN(day_diff)) {
|
||||
// Woohh we are on the future here
|
||||
if (day_diff < 0) {
|
||||
prettyDate = (new Date(time)).toLocaleString(navigator.languages, {
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
} else {
|
||||
if (day_diff === 0) {
|
||||
prettyDate = _('today');
|
||||
} else if (day_diff === 1) {
|
||||
prettyDate = _('yesterday');
|
||||
} else if (day_diff < 6) {
|
||||
prettyDate = (new Date(time)).toLocaleString(navigator.languages, {
|
||||
weekday: 'long'
|
||||
});
|
||||
} else {
|
||||
prettyDate = (new Date(time)).toLocaleString(navigator.languages, {
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var timeString = (new Date(time)).toLocaleString(navigator.languages, {
|
||||
hour12: navigator.mozHour12,
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
});
|
||||
|
||||
//XXX: This order should not be hardcoded. Change once we have
|
||||
// relative date/time in Intl
|
||||
return prettyDate + ' ' + timeString;
|
||||
};
|
||||
})();
|
||||
}
|
||||
37
gaia/apps/communications/contacts/js/utilities/performance_helper.js
Executable file
37
gaia/apps/communications/contacts/js/utilities/performance_helper.js
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Utility class to fire events needed to measure the performance
|
||||
* of the application.
|
||||
* For an explanation of what means each event go here:
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=996038
|
||||
* For an explanation of how are adapted to the specific needs
|
||||
* in the contacts app please go here:
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1015388#c8
|
||||
*/
|
||||
(function(){
|
||||
|
||||
window.utils = window.utils || {};
|
||||
|
||||
var PerformanceHelper = {
|
||||
domLoaded: function() {
|
||||
window.performance.mark('navigationLoaded');
|
||||
},
|
||||
chromeInteractive: function() {
|
||||
window.performance.mark('navigationInteractive');
|
||||
},
|
||||
visuallyComplete: function() {
|
||||
window.performance.mark('visuallyLoaded');
|
||||
},
|
||||
contentInteractive: function() {
|
||||
window.performance.mark('contentInteractive');
|
||||
},
|
||||
loadEnd: function() {
|
||||
window.performance.mark('fullyLoaded');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
window.utils.PerformanceHelper = PerformanceHelper;
|
||||
|
||||
})();
|
||||
150
gaia/apps/communications/contacts/js/utilities/sim_dom_generator.js
Executable file
150
gaia/apps/communications/contacts/js/utilities/sim_dom_generator.js
Executable file
|
|
@ -0,0 +1,150 @@
|
|||
/* global _ */
|
||||
'use strict';
|
||||
|
||||
// Generates the DOM for each button, depending
|
||||
// on different variables
|
||||
function generateButton(singleSim, sim, index) {
|
||||
var button = document.createElement('button');
|
||||
button.classList.add('icon', 'icon-sim');
|
||||
|
||||
var span = document.createElement('span');
|
||||
if (singleSim) {
|
||||
span.setAttribute('data-l10n-id', 'simCard');
|
||||
} else {
|
||||
var msisdn = sim.iccInfo && sim.iccInfo.msisdn;
|
||||
var l10nId = msisdn ? 'simNumberWithMSISDN' : 'simNumberNoMSISDN';
|
||||
var l10nArgs = {
|
||||
number: index,
|
||||
msisdn: msisdn
|
||||
};
|
||||
navigator.mozL10n.setAttributes(span, l10nId, l10nArgs);
|
||||
}
|
||||
button.appendChild(span);
|
||||
return button;
|
||||
}
|
||||
|
||||
var SimDomGenerator = function SimDomGenerator() {
|
||||
};
|
||||
|
||||
SimDomGenerator.prototype.setIccList = function(list) {
|
||||
this.sims = list;
|
||||
this.singleSim = (list.length === 1);
|
||||
};
|
||||
|
||||
SimDomGenerator.prototype.generateDOM = function() {
|
||||
this.cleanDOM();
|
||||
this.generateExportDOM();
|
||||
this.generateImportDOM();
|
||||
};
|
||||
|
||||
// Specific DOM generation for export options, generating the
|
||||
// following code:
|
||||
//<li id="export-sim-option-<id>" data-source="sim">
|
||||
// <button class="icon icon-sim">
|
||||
// <span data-l10n-id="importSim2">SIM <number> (optional : msisdn)</span>
|
||||
// <p><span></span><time></time></p>
|
||||
// </button>
|
||||
// <p class="error-message" data-l10n-id="noSimMsgExport"></p>
|
||||
//</li>
|
||||
SimDomGenerator.prototype.generateExportDOM = function() {
|
||||
var exportList = document.getElementById('export-options');
|
||||
if (exportList === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var firstOption = exportList.firstChild;
|
||||
|
||||
var self = this;
|
||||
this.sims.forEach(function onSim(sim, index) {
|
||||
if (sim === null) {
|
||||
return;
|
||||
}
|
||||
var iccId = sim.iccInfo.iccid;
|
||||
var li = document.createElement('li');
|
||||
li.id = 'export-sim-option-' + iccId;
|
||||
li.dataset.source = 'sim';
|
||||
li.dataset.iccid = iccId;
|
||||
li.setAttribute('role', 'presentation');
|
||||
|
||||
var button = generateButton(self.singleSim, sim, index + 1);
|
||||
button.setAttribute('role', 'option');
|
||||
|
||||
var p = document.createElement('p');
|
||||
p.classList.add('error-message');
|
||||
p.textContent = _('noSimMsgExport');
|
||||
|
||||
li.appendChild(button);
|
||||
li.appendChild(p);
|
||||
|
||||
exportList.insertBefore(li, firstOption);
|
||||
});
|
||||
};
|
||||
|
||||
// Specific DOM generation for import options, generating the
|
||||
// following code:
|
||||
//<li id="import-sim-option-<id>" data-source="sim-<iccId>">
|
||||
// <button class="icon icon-sim" data-l10n-id="importSim2">
|
||||
// SIM <number> (optional : msisdn)
|
||||
// <p><span></span><time></time></p>
|
||||
// </button>
|
||||
// <p class="error-message" data-l10n-id="noSimMsg"></p>
|
||||
//</li>
|
||||
SimDomGenerator.prototype.generateImportDOM = function() {
|
||||
var importList = document.getElementById('import-options');
|
||||
if (importList === null) {
|
||||
return;
|
||||
}
|
||||
var firstOption = importList.firstChild;
|
||||
|
||||
var self = this;
|
||||
this.sims.forEach(function onSim(sim, index) {
|
||||
if (sim === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var iccId = sim.iccInfo.iccid;
|
||||
var li = document.createElement('li');
|
||||
li.dataset.source = 'sim-' + iccId;
|
||||
li.dataset.iccid = iccId;
|
||||
li.id = 'import-sim-option-' + iccId;
|
||||
li.setAttribute('role', 'presentation');
|
||||
|
||||
var button = generateButton(self.singleSim, sim, index + 1);
|
||||
button.setAttribute('role', 'option');
|
||||
|
||||
var pTime = document.createElement('p');
|
||||
pTime.appendChild(document.createElement('span'));
|
||||
pTime.appendChild(document.createElement('time'));
|
||||
|
||||
button.appendChild(pTime);
|
||||
|
||||
var p = document.createElement('p');
|
||||
p.classList.add('error-message');
|
||||
p.textContent = _('noSimMsg');
|
||||
|
||||
li.appendChild(button);
|
||||
li.appendChild(p);
|
||||
|
||||
importList.insertBefore(li, firstOption);
|
||||
});
|
||||
};
|
||||
|
||||
// Clean any dom elements generated with this class
|
||||
SimDomGenerator.prototype.cleanDOM = function() {
|
||||
// Clean the generated DOM
|
||||
function clean(selector) {
|
||||
var items = Array.prototype.slice.call(
|
||||
document.querySelectorAll(selector));
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
items.forEach(function(node) {
|
||||
node.parentNode.removeChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
// Import
|
||||
clean('#import-options [data-iccid]');
|
||||
// Export
|
||||
clean('#export-options [data-iccid]');
|
||||
};
|
||||
108
gaia/apps/communications/contacts/js/utilities/tagSelector.js
Executable file
108
gaia/apps/communications/contacts/js/utilities/tagSelector.js
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
(function(exports) {
|
||||
|
||||
/* global TAG_OPTIONS, ContactsTag, MainNavigation, LazyLoader */
|
||||
/* exported TagSelector*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var customTag, customTagReset, tagDone, tagHeader, lazyLoadedTagsDom = false;
|
||||
var contactTag;
|
||||
|
||||
function handleCustomTagReset(ev) {
|
||||
ev.preventDefault();
|
||||
if (customTag) {
|
||||
customTag.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function handleCustomTag(ev) {
|
||||
if (ev.keyCode === 13) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
ContactsTag.touchCustomTag();
|
||||
}
|
||||
|
||||
function handleBack(cb) {
|
||||
MainNavigation.back(cb);
|
||||
}
|
||||
|
||||
function handleSelectTagDone() {
|
||||
var prevValue = contactTag.textContent;
|
||||
ContactsTag.clickDone(function() {
|
||||
var valueModifiedEvent = new CustomEvent('ValueModified', {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
prevValue: prevValue,
|
||||
newValue: contactTag.textContent
|
||||
}
|
||||
});
|
||||
contactTag.dispatchEvent(valueModifiedEvent);
|
||||
handleBack();
|
||||
});
|
||||
}
|
||||
|
||||
function showSelectTag() {
|
||||
var tagsList = document.getElementById('tags-list');
|
||||
var selectedTagType = contactTag.dataset.taglist;
|
||||
var options = TAG_OPTIONS[selectedTagType];
|
||||
|
||||
var type = selectedTagType.split('-')[0];
|
||||
var isCustomTagVisible = (document.querySelector(
|
||||
'[data-template]' + '.' + type + '-' +
|
||||
'template').dataset.custom != 'false');
|
||||
|
||||
options = ContactsTag.filterTags(type, contactTag, options);
|
||||
|
||||
if (!customTag) {
|
||||
customTag = document.querySelector('#custom-tag');
|
||||
customTag.addEventListener('keydown', handleCustomTag);
|
||||
customTag.addEventListener('touchend', handleCustomTag);
|
||||
}
|
||||
if (!customTagReset) {
|
||||
customTagReset = document.getElementById('custom-tag-reset');
|
||||
customTagReset.addEventListener('touchstart', handleCustomTagReset);
|
||||
}
|
||||
if (!tagDone) {
|
||||
tagDone = document.querySelector('#settings-done');
|
||||
tagDone.addEventListener('click', handleSelectTagDone);
|
||||
}
|
||||
if (!tagHeader) {
|
||||
tagHeader = document.querySelector('#settings-header');
|
||||
tagHeader.addEventListener('action', handleBack);
|
||||
}
|
||||
|
||||
ContactsTag.setCustomTag(customTag);
|
||||
// Set whether the custom tag is visible or not
|
||||
// This is needed for dates as we only support bday and anniversary
|
||||
// and not custom dates
|
||||
ContactsTag.setCustomTagVisibility(isCustomTagVisible);
|
||||
|
||||
ContactsTag.fillTagOptions(tagsList, contactTag, options);
|
||||
|
||||
MainNavigation.go('view-select-tag', 'right-left');
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
var TagSelector = {
|
||||
|
||||
show: function goToSelectTag(pContactTag) {
|
||||
contactTag = pContactTag;
|
||||
|
||||
var tagViewElement = document.getElementById('view-select-tag');
|
||||
if (!lazyLoadedTagsDom) {
|
||||
LazyLoader.load(tagViewElement, function() {
|
||||
showSelectTag();
|
||||
lazyLoadedTagsDom = true;
|
||||
});
|
||||
}
|
||||
else {
|
||||
showSelectTag();
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
exports.TagSelector = TagSelector;
|
||||
}(window));
|
||||
45
gaia/apps/communications/contacts/js/utilities/telemetry.js
Executable file
45
gaia/apps/communications/contacts/js/utilities/telemetry.js
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Copyright 2015, Mozilla Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
/* global AdvancedTelemetryHelper */
|
||||
/* global LazyLoader */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var namespace = 'telemetry_gaia_contacts';
|
||||
|
||||
var dependencies = LazyLoader.load(
|
||||
['/shared/js/settings_listener.js',
|
||||
'/shared/js/advanced_telemetry_helper.js']);
|
||||
|
||||
var Telemetry = {
|
||||
logImportUsage: function(name) {
|
||||
return dependencies.then(() => {
|
||||
var histogramName = namespace + '_import_' + name;
|
||||
var count = new AdvancedTelemetryHelper(
|
||||
AdvancedTelemetryHelper.HISTOGRAM_COUNT,
|
||||
histogramName);
|
||||
count.add(1);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.Telemetry = Telemetry;
|
||||
// For testing purposes
|
||||
exports.TelemetryReady = dependencies;
|
||||
|
||||
})(window);
|
||||
641
gaia/apps/communications/contacts/js/views/details.js
Executable file
641
gaia/apps/communications/contacts/js/views/details.js
Executable file
|
|
@ -0,0 +1,641 @@
|
|||
'use strict';
|
||||
|
||||
/* jshint nonew: false */
|
||||
|
||||
/* global ActivityHandler */
|
||||
/* global COMMS_APP_ORIGIN */
|
||||
/* global Contacts */
|
||||
/* global ContactsButtons */
|
||||
/* global ContactPhotoHelper */
|
||||
/* globals ContactToVcardBlob */
|
||||
/* global fb */
|
||||
/* global NFC */
|
||||
/* global ICEData */
|
||||
/* global LazyLoader */
|
||||
/* global MozActivity */
|
||||
/* global Normalizer */
|
||||
/* global SCALE_RATIO */
|
||||
/* global TAG_OPTIONS */
|
||||
/* global utils */
|
||||
/* global VcardFilename */
|
||||
/* global MatchService */
|
||||
/* global WebrtcClient */
|
||||
/* global MainNavigation */
|
||||
/* global ContactsService */
|
||||
|
||||
var contacts = window.contacts || {};
|
||||
|
||||
contacts.Details = (function() {
|
||||
var photoPos = 7;
|
||||
var initMargin = 8;
|
||||
var DEFAULT_TEL_TYPE = 'other';
|
||||
// If it is a favourite on/off change, I cancel the render
|
||||
var isAFavoriteChange = false;
|
||||
var contactData,
|
||||
contactDetails,
|
||||
listContainer,
|
||||
detailsName,
|
||||
detailsNameText,
|
||||
orgTitle,
|
||||
datesTemplate,
|
||||
addressesTemplate,
|
||||
socialTemplate,
|
||||
duplicateTemplate,
|
||||
notesTemplate,
|
||||
isFbContact,
|
||||
isFbLinked,
|
||||
editContactButton,
|
||||
cover,
|
||||
favoriteMessage,
|
||||
detailsInner,
|
||||
dom,
|
||||
currentSocial,
|
||||
header,
|
||||
_;
|
||||
|
||||
var init = function cd_init(currentDom) {
|
||||
_ = navigator.mozL10n.get;
|
||||
dom = currentDom || document;
|
||||
header = dom.querySelector('#details-view-header');
|
||||
contactDetails = dom.querySelector('#contact-detail');
|
||||
listContainer = dom.querySelector('#details-list');
|
||||
detailsName = dom.querySelector('#contact-name-title');
|
||||
detailsNameText = dom.querySelector('#contact-name-title bdi');
|
||||
orgTitle = dom.querySelector('#org-title');
|
||||
datesTemplate = dom.querySelector('#dates-template-\\#i\\#');
|
||||
addressesTemplate = dom.querySelector('#address-details-template-\\#i\\#');
|
||||
socialTemplate = dom.querySelector('#social-template-\\#i\\#');
|
||||
duplicateTemplate = dom.querySelector('#duplicate-contacts-template');
|
||||
editContactButton = dom.querySelector('#edit-contact-button');
|
||||
cover = dom.querySelector('#cover-img');
|
||||
detailsInner = dom.querySelector('#contact-detail-inner');
|
||||
favoriteMessage = dom.querySelector('#toggle-favorite');
|
||||
notesTemplate = dom.querySelector('#note-details-template-\\#i\\#');
|
||||
|
||||
initPullEffect(cover);
|
||||
|
||||
// to avoid race conditions with NFC, we load it before handleDetails
|
||||
LazyLoader.load('/contacts/js/nfc.js', () => {
|
||||
favoriteMessage.addEventListener('click', toggleFavorite);
|
||||
editContactButton.addEventListener('click', showEditContact);
|
||||
header.addEventListener('action', handleDetailsBack);
|
||||
});
|
||||
|
||||
ContactsButtons.init(listContainer, contactDetails, ActivityHandler);
|
||||
};
|
||||
|
||||
var getWebrtcClientResources = function getWebrtcClientResources(cb) {
|
||||
if (typeof cb !== 'function') {
|
||||
return;
|
||||
}
|
||||
LazyLoader.load(
|
||||
[
|
||||
'/contacts/style/webrtc-client/webrtc_client.css',
|
||||
'/contacts/js/webrtc-client/webrtc_client.js'
|
||||
],
|
||||
cb
|
||||
);
|
||||
};
|
||||
|
||||
var handleDetailsBack = function handleDetailsBack() {
|
||||
// disable NFC listeners when going out of Details view
|
||||
stopNFC();
|
||||
|
||||
if (WebrtcClient) {
|
||||
getWebrtcClientResources(WebrtcClient.stop);
|
||||
}
|
||||
|
||||
if (ActivityHandler.currentActivityIsNot(['import'])) {
|
||||
ActivityHandler.postCancel();
|
||||
MainNavigation.home();
|
||||
}
|
||||
else if (window.ICEView && window.ICEView.iceListDisplayed) {
|
||||
ICEData.getActiveIceContacts().then(function(list) {
|
||||
if (!Array.isArray(list) || list.length === 0) {
|
||||
MainNavigation.home();
|
||||
}
|
||||
else {
|
||||
doHandleDetailsBack();
|
||||
}
|
||||
}, doHandleDetailsBack);
|
||||
}
|
||||
else {
|
||||
doHandleDetailsBack();
|
||||
}
|
||||
};
|
||||
|
||||
var doHandleDetailsBack = function() {
|
||||
var hashParams = window.location.hash.split('?');
|
||||
var params = hashParams.length > 1 ?
|
||||
utils.extractParams(hashParams[1]) : -1;
|
||||
|
||||
// post message to parent page included Contacts app.
|
||||
if (params.back_to_previous_tab === '1') {
|
||||
var message = { 'type': 'contactsiframe', 'message': 'back' };
|
||||
window.parent.postMessage(message, COMMS_APP_ORIGIN);
|
||||
} else {
|
||||
MainNavigation.back(resetPhoto);
|
||||
}
|
||||
};
|
||||
|
||||
var showEditContact = function showEditContact() {
|
||||
// Disable NFC listeners when editing a contact
|
||||
stopNFC();
|
||||
Contacts.showForm(true, contactData);
|
||||
};
|
||||
|
||||
var setContact = function cd_setContact(currentContact) {
|
||||
contactData = currentContact;
|
||||
startNFC(currentContact);
|
||||
};
|
||||
|
||||
// Needed for now because of external call from contacts.js
|
||||
var startNFC = function(contact) {
|
||||
LazyLoader.load('/contacts/js/nfc.js', () => {
|
||||
NFC.startListening(contact);
|
||||
});
|
||||
};
|
||||
|
||||
var stopNFC = function() {
|
||||
LazyLoader.load('/contacts/js/nfc.js', () => {
|
||||
NFC.stopListening();
|
||||
});
|
||||
};
|
||||
|
||||
var initPullEffect = function cd_initPullEffect(cover) {
|
||||
var maxPosition = Math.round(150 * SCALE_RATIO);
|
||||
var startPosition = 0;
|
||||
|
||||
function onTouchStart(e) {
|
||||
if (contactDetails.classList.contains('no-photo')) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
startPosition = e.changedTouches[0].clientY;
|
||||
|
||||
contactDetails.classList.add('up');
|
||||
cover.classList.add('up');
|
||||
|
||||
window.addEventListener('touchmove', onTouchMove, true);
|
||||
window.addEventListener('touchend', onTouchEnd, true);
|
||||
}
|
||||
|
||||
function onTouchEnd(e) {
|
||||
e.preventDefault();
|
||||
|
||||
contactDetails.style.transform = null;
|
||||
contactDetails.classList.add('up');
|
||||
|
||||
cover.style.transform = null;
|
||||
cover.classList.add('up');
|
||||
|
||||
window.removeEventListener('touchmove', onTouchMove, true);
|
||||
window.removeEventListener('touchend', onTouchEnd, true);
|
||||
}
|
||||
|
||||
function onTouchMove(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var deltaY = e.changedTouches[0].clientY - startPosition;
|
||||
deltaY = Math.min(maxPosition, Math.max(0, deltaY));
|
||||
|
||||
var calc = 'calc(' + initMargin + 'rem + ' + deltaY + 'px)';
|
||||
contactDetails.style.transform = 'translateY(' + calc + ')';
|
||||
contactDetails.classList.remove('up');
|
||||
|
||||
// Divide by 40 (4 times slower and in rems)
|
||||
var coverPosition = (-photoPos + (deltaY / 40)) + 'rem';
|
||||
cover.style.transform = 'translateY(' + coverPosition + ')';
|
||||
cover.classList.remove('up');
|
||||
}
|
||||
|
||||
cover.addEventListener('touchstart', onTouchStart, true);
|
||||
};
|
||||
|
||||
// readOnly tells us if we should allow editing the rendered contact.
|
||||
var render = function cd_render(currentContact, fbContactData, readOnly) {
|
||||
|
||||
if(isAFavoriteChange){
|
||||
isAFavoriteChange = false;
|
||||
return Promise.resolve(isAFavoriteChange);
|
||||
}
|
||||
|
||||
|
||||
contactData = currentContact || contactData;
|
||||
|
||||
startNFC(contactData);
|
||||
|
||||
isFbContact = fb.isFbContact(contactData);
|
||||
isFbLinked = fb.isFbLinked(contactData);
|
||||
|
||||
// Initially enabled and only disabled if necessary
|
||||
editContactButton.removeAttribute('disabled');
|
||||
editContactButton.classList.remove('hide');
|
||||
header.setAttribute('action', 'back');
|
||||
socialTemplate.classList.remove('hide');
|
||||
|
||||
if (readOnly) {
|
||||
editContactButton.classList.add('hide');
|
||||
header.setAttribute('action', 'close');
|
||||
socialTemplate.classList.add('hide');
|
||||
}
|
||||
|
||||
if (!fbContactData && isFbContact) {
|
||||
var fbContact = new fb.Contact(contactData);
|
||||
var req = fbContact.getData();
|
||||
|
||||
req.onsuccess = function do_reload() {
|
||||
doReloadContactDetails(req.result);
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
window.console.error('FB: Error while loading FB contact data');
|
||||
doReloadContactDetails(contactData);
|
||||
};
|
||||
} else {
|
||||
doReloadContactDetails(fbContactData || contactData);
|
||||
}
|
||||
};
|
||||
|
||||
// Fills the contact data to display if no givenName and familyName
|
||||
var getDisplayName = function getDisplayName(contact) {
|
||||
var name = _('noName');
|
||||
|
||||
if (hasName(contact)) {
|
||||
name = contact.name[0];
|
||||
} else if (hasContent(contact.tel)) {
|
||||
name = contact.tel[0].value;
|
||||
} else if (hasContent(contact.email)) {
|
||||
name = contact.email[0].value;
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
function hasContent(field) {
|
||||
return (Array.isArray(field) &&
|
||||
field.length > 0 &&
|
||||
field[0].value &&
|
||||
field[0].value.trim());
|
||||
}
|
||||
|
||||
function hasName(contact) {
|
||||
return (Array.isArray(contact.name) &&
|
||||
contact.name[0] &&
|
||||
contact.name[0].trim());
|
||||
}
|
||||
|
||||
//
|
||||
// Method that generates HTML markup for the contact
|
||||
//
|
||||
var doReloadContactDetails = function doReloadContactDetails(contact) {
|
||||
detailsNameText.textContent = getDisplayName(contact);
|
||||
contactDetails.classList.remove('no-photo');
|
||||
contactDetails.classList.remove('fb-contact');
|
||||
contactDetails.classList.remove('up');
|
||||
utils.dom.removeChildNodes(listContainer);
|
||||
|
||||
renderFavorite(contact);
|
||||
renderOrg(contact);
|
||||
|
||||
ContactsButtons.renderPhones(contact);
|
||||
ContactsButtons.renderEmails(contact);
|
||||
|
||||
renderWebrtcClient(contactData);// Don't share the FB info
|
||||
|
||||
renderAddresses(contact);
|
||||
|
||||
renderDates(contact);
|
||||
|
||||
renderNotes(contact);
|
||||
|
||||
renderShareButton(contact);
|
||||
|
||||
if (!fb.isFbContact(contact) || fb.isFbLinked(contact)) {
|
||||
renderDuplicate(contact);
|
||||
}
|
||||
|
||||
renderPhoto(contact);
|
||||
};
|
||||
|
||||
var renderFavorite = function cd_renderFavorite(contact) {
|
||||
var favorite = isFavorite(contact);
|
||||
toggleFavoriteMessage(favorite);
|
||||
|
||||
header.classList.toggle('favorite', !!favorite);
|
||||
};
|
||||
|
||||
var isFavorite = function isFavorite(contact) {
|
||||
return contact != null && contact.category != null &&
|
||||
contact.category.indexOf('favorite') != -1;
|
||||
};
|
||||
|
||||
var toggleFavorite = function toggleFavorite() {
|
||||
var contact = contactData;
|
||||
|
||||
var favorite = !isFavorite(contact);
|
||||
toggleFavoriteMessage(favorite);
|
||||
if (favorite) {
|
||||
contact.category = contact.category || [];
|
||||
contact.category.push('favorite');
|
||||
} else {
|
||||
if (!contact.category) {
|
||||
return;
|
||||
}
|
||||
var pos = contact.category.indexOf('favorite');
|
||||
if (pos > -1) {
|
||||
contact.category.splice(pos, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Disabling button while saving the contact
|
||||
favoriteMessage.style.pointerEvents = 'none';
|
||||
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
ContactsService.save(
|
||||
utils.misc.toMozContact(contact),
|
||||
function(e) {
|
||||
if (e) {
|
||||
favoriteMessage.style.pointerEvents = 'auto';
|
||||
console.error('Error saving favorite');
|
||||
reject('Error saving favorite');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
isAFavoriteChange = true;
|
||||
ContactsService.get(
|
||||
contact.id,
|
||||
function onSuccess(savedContact) {
|
||||
renderFavorite(savedContact);
|
||||
setContact(savedContact);
|
||||
favoriteMessage.style.pointerEvents = 'auto';
|
||||
},
|
||||
function onError() {
|
||||
console.error('Error reloading contact');
|
||||
favoriteMessage.style.pointerEvents = 'auto';
|
||||
}
|
||||
);
|
||||
resolve(isAFavoriteChange);
|
||||
}
|
||||
);
|
||||
}).then();
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
var toggleFavoriteMessage = function toggleFavMessage(isFav) {
|
||||
var cList = favoriteMessage.classList;
|
||||
var l10nId = isFav ? 'removeFavorite' : 'addFavorite';
|
||||
favoriteMessage.setAttribute('data-l10n-id', l10nId);
|
||||
isFav ? cList.add('on') : cList.remove('on');
|
||||
};
|
||||
|
||||
var renderOrg = function cd_renderOrg(contact) {
|
||||
if (contact.org && contact.org.length > 0 && contact.org[0] !== '') {
|
||||
orgTitle.textContent = contact.org[0];
|
||||
orgTitle.classList.remove('hide');
|
||||
} else {
|
||||
orgTitle.classList.add('hide');
|
||||
orgTitle.textContent = '';
|
||||
}
|
||||
};
|
||||
|
||||
var renderDates = function cd_renderDates(contact) {
|
||||
if (!contact.bday && !contact.anniversary) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dateString = '';
|
||||
|
||||
var fields = ['bday', 'anniversary'];
|
||||
var l10nIds = ['birthday', 'anniversary'];
|
||||
|
||||
var rendered = 0;
|
||||
for (var j = 0; j < fields.length; j++) {
|
||||
var value = contact[fields[j]];
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
dateString = utils.misc.formatDate(value);
|
||||
} catch (err) {
|
||||
console.error('Error parsing date');
|
||||
continue;
|
||||
}
|
||||
var element = utils.templates.render(datesTemplate, {
|
||||
i: ++rendered,
|
||||
date: dateString,
|
||||
type: _(l10nIds[j])
|
||||
});
|
||||
|
||||
listContainer.appendChild(element);
|
||||
}
|
||||
};
|
||||
|
||||
var renderShareButton = function cd_renderShareButton(contact) {
|
||||
var social = utils.templates.render(socialTemplate, {
|
||||
i: contact.id
|
||||
});
|
||||
currentSocial = social;
|
||||
var shareButton = social.querySelector('#share_button');
|
||||
|
||||
shareButton.addEventListener('click', shareContact);
|
||||
shareButton.classList.remove('hide');
|
||||
|
||||
listContainer.appendChild(social);
|
||||
};
|
||||
|
||||
var renderWebrtcClient = function renderWebrtcClient(contact) {
|
||||
getWebrtcClientResources(function onLoaded() {
|
||||
WebrtcClient.start(contact);
|
||||
});
|
||||
};
|
||||
|
||||
var renderAddresses = function cd_renderAddresses(contact) {
|
||||
if (!contact.adr) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < contact.adr.length; i++) {
|
||||
var currentAddress = contact.adr[i];
|
||||
// Sanity check
|
||||
if (utils.mozContact.haveEmptyFields(currentAddress,
|
||||
['streetAddress', 'postalCode', 'locality', 'countryName'])) {
|
||||
continue;
|
||||
}
|
||||
var address = currentAddress.streetAddress || '';
|
||||
var escapedStreet = Normalizer.escapeHTML(address, true);
|
||||
var locality = currentAddress.locality;
|
||||
var escapedLocality = Normalizer.escapeHTML(locality, true);
|
||||
var escapedType = Normalizer.escapeHTML(currentAddress.type, true);
|
||||
var country = currentAddress.countryName || '';
|
||||
var escapedCountry = Normalizer.escapeHTML(country, true);
|
||||
var postalCode = currentAddress.postalCode || '';
|
||||
var escapedPostalCode = Normalizer.escapeHTML(postalCode, true);
|
||||
|
||||
var addressField = {
|
||||
streetAddress: escapedStreet,
|
||||
postalCode: escapedPostalCode,
|
||||
locality: escapedLocality || '',
|
||||
countryName: escapedCountry,
|
||||
type: _(escapedType) || escapedType ||
|
||||
TAG_OPTIONS['address-type'][0].value,
|
||||
'type_l10n_id': currentAddress.type,
|
||||
i: i
|
||||
};
|
||||
var template = utils.templates.render(addressesTemplate, addressField);
|
||||
listContainer.appendChild(template);
|
||||
}
|
||||
};
|
||||
|
||||
var renderDuplicate = function cd_renderDuplicate(contact) {
|
||||
var dupItem = utils.templates.render(duplicateTemplate, {});
|
||||
var findMergeButton = dupItem.querySelector('#find-merge-button');
|
||||
findMergeButton.disabled = true;
|
||||
|
||||
if (contacts.List.total > 1) {
|
||||
// Only have this active if contact list has more than one entry
|
||||
findMergeButton.disabled = false;
|
||||
findMergeButton.addEventListener('click', function finding() {
|
||||
MatchService.match(contact.id);
|
||||
});
|
||||
}
|
||||
|
||||
listContainer.appendChild(dupItem);
|
||||
};
|
||||
|
||||
var renderNotes = function cd_rederNotes(contact) {
|
||||
if (!contact.note || contact.note.length === 0) {
|
||||
return;
|
||||
}
|
||||
var container = document.createElement('li');
|
||||
var title = document.createElement('h2');
|
||||
title.setAttribute('data-l10n-id', 'comments');
|
||||
container.appendChild(title);
|
||||
for (var i = 0; i < contact.note.length; i++) {
|
||||
var currentNote = contact.note[i];
|
||||
var noteField = {
|
||||
note: Normalizer.escapeHTML(currentNote, true) || '',
|
||||
i: i
|
||||
};
|
||||
var template = utils.templates.render(notesTemplate, noteField);
|
||||
container.appendChild(template);
|
||||
listContainer.appendChild(container);
|
||||
}
|
||||
};
|
||||
|
||||
var calculateHash = function calculateHash(photo, cb) {
|
||||
var START_BYTES = 127;
|
||||
var BYTES_HASH = 16;
|
||||
|
||||
var out = [photo.type, photo.size];
|
||||
|
||||
// We skip the first bytes that typically are headers
|
||||
var chunk = photo.slice(START_BYTES, START_BYTES + BYTES_HASH);
|
||||
var reader = new FileReader();
|
||||
reader.onloadend = function() {
|
||||
out.push(reader.result);
|
||||
cb(out.join(''));
|
||||
};
|
||||
reader.onerror = function() {
|
||||
window.console.error('Error while calculating the hash: ',
|
||||
reader.error.name);
|
||||
cb(out.join(''));
|
||||
};
|
||||
reader.readAsDataURL(chunk);
|
||||
};
|
||||
|
||||
var updateHash = function updateHash(photo, cover) {
|
||||
calculateHash(photo, function(hash) {
|
||||
cover.dataset.imgHash = hash;
|
||||
});
|
||||
};
|
||||
|
||||
var renderPhoto = function cd_renderPhoto(contact) {
|
||||
contactDetails.classList.remove('up');
|
||||
if (isFbContact) {
|
||||
contactDetails.classList.add('fb-contact');
|
||||
}
|
||||
|
||||
var photo = ContactPhotoHelper.getFullResolution(contact);
|
||||
if (photo) {
|
||||
var currentHash = cover.dataset.imgHash;
|
||||
if (!currentHash) {
|
||||
utils.dom.updatePhoto(photo, cover);
|
||||
updateHash(photo, cover);
|
||||
}
|
||||
else {
|
||||
// Need to recalculate the hash and see whether the images changed
|
||||
calculateHash(photo, function(newHash) {
|
||||
if (currentHash !== newHash) {
|
||||
utils.dom.updatePhoto(photo, cover);
|
||||
cover.dataset.imgHash = newHash;
|
||||
}
|
||||
else {
|
||||
// Only for testing purposes
|
||||
cover.dataset.photoReady = 'true';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
contactDetails.classList.add('up');
|
||||
cover.classList.add('translated');
|
||||
contactDetails.classList.add('translated');
|
||||
var clientHeight = contactDetails.clientHeight -
|
||||
(initMargin * 10 * SCALE_RATIO);
|
||||
if (detailsInner.offsetHeight < clientHeight) {
|
||||
cover.style.overflow = 'hidden';
|
||||
} else {
|
||||
cover.style.overflow = 'auto';
|
||||
}
|
||||
} else {
|
||||
resetPhoto();
|
||||
}
|
||||
};
|
||||
|
||||
var resetPhoto = function cd_resetPhoto() {
|
||||
cover.classList.remove('translated');
|
||||
contactDetails.classList.remove('translated');
|
||||
cover.style.backgroundImage = '';
|
||||
cover.style.overflow = 'auto';
|
||||
contactDetails.style.transform = '';
|
||||
contactDetails.classList.add('no-photo');
|
||||
cover.dataset.imgHash = '';
|
||||
};
|
||||
|
||||
var shareContact = function cd_shareContact() {
|
||||
const VCARD_DEPS = [
|
||||
'/shared/js/text_normalizer.js',
|
||||
'/shared/js/contact2vcard.js',
|
||||
'/shared/js/setImmediate.js'
|
||||
];
|
||||
|
||||
LazyLoader.load(VCARD_DEPS,function vcardLoaded() {
|
||||
ContactToVcardBlob([contactData], function blobReady(vcardBlob) {
|
||||
VcardFilename(contactData).then(filename => {
|
||||
new MozActivity({
|
||||
name: 'share',
|
||||
data: {
|
||||
type: 'text/vcard',
|
||||
number: 1,
|
||||
blobs: [new window.File([vcardBlob], filename, {
|
||||
type: 'text/x-vcard'
|
||||
})],
|
||||
filenames: [filename]
|
||||
}
|
||||
});
|
||||
});
|
||||
// The MIME of the blob should be this for some MMS gateways
|
||||
}, { type: 'text/x-vcard'} );
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
'init': init,
|
||||
'setContact': setContact,
|
||||
'toggleFavorite': toggleFavorite,
|
||||
'render': render,
|
||||
'defaultTelType': DEFAULT_TEL_TYPE,
|
||||
'startNFC': startNFC
|
||||
};
|
||||
})();
|
||||
1507
gaia/apps/communications/contacts/js/views/form.js
Executable file
1507
gaia/apps/communications/contacts/js/views/form.js
Executable file
File diff suppressed because it is too large
Load diff
108
gaia/apps/communications/contacts/js/views/ice.js
Executable file
108
gaia/apps/communications/contacts/js/views/ice.js
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
/* global ICEData */
|
||||
/* global ImageLoader */
|
||||
/* global LazyLoader */
|
||||
/* global MainNavigation */
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var contactsIds = null;
|
||||
var rowBuilderFn = null;
|
||||
var imgLoader = null;
|
||||
var initialized = false;
|
||||
var iceListDisplayed = false;
|
||||
|
||||
/**
|
||||
* Initialises the component by passing a list of ICE ids,
|
||||
* a function that knows how to build contact list rows and
|
||||
* another function to perform actions when clicking on a
|
||||
* ICE contact.
|
||||
* This function should be the same than the contacts list one
|
||||
* to keep the same behavior (activities, select, etc.)
|
||||
* @param ids (Array) array of contact ids
|
||||
* @param rowBuilder (function) function that paints contac list
|
||||
* rows, shared with the contact list
|
||||
* @param clickHandler (function) action to perform when clicked
|
||||
* on a item
|
||||
*/
|
||||
function init(ids, rowBuilder, clickHandler) {
|
||||
rowBuilderFn = rowBuilder;
|
||||
contactsIds = ids;
|
||||
|
||||
imgLoader = new ImageLoader('#ice-list', 'li');
|
||||
buildICEContactsList();
|
||||
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
|
||||
// Initialise common controls
|
||||
document.getElementById('ice-header').addEventListener(
|
||||
'action', hideICEList);
|
||||
document.getElementById('ice-list').addEventListener('click', clickHandler);
|
||||
|
||||
LazyLoader.load(['/contacts/js/utilities/ice_data.js'], function() {
|
||||
listenForChanges();
|
||||
});
|
||||
}
|
||||
|
||||
// Once the list is built, we need to be aware of possible changes in
|
||||
// contacts. In that case rebuild the whole list, since ICE list is meant
|
||||
// to be a small set (2 currently) of contacts.
|
||||
function listenForChanges() {
|
||||
ICEData.listenForChanges(function(newList) {
|
||||
contactsIds = [];
|
||||
newList.forEach(function(iceContact) {
|
||||
if (iceContact.id && iceContact.active) {
|
||||
contactsIds.push(iceContact.id);
|
||||
}
|
||||
});
|
||||
buildICEContactsList();
|
||||
});
|
||||
}
|
||||
|
||||
// Clone the nodes on the contacts list, also rebuild them with
|
||||
// the row builder function passed on initialization.
|
||||
// Due to the nature of the contacts list, despite of having the
|
||||
// nodes, those may have not being initialized, that's why we need
|
||||
// the use of this builder function
|
||||
function buildICEContactsList() {
|
||||
var iceList = document.getElementById('ice-group');
|
||||
iceList.innerHTML = '';
|
||||
|
||||
contactsIds.forEach(function(id) {
|
||||
var node = document.querySelector('[data-uuid="' + id + '"]');
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
node = rowBuilderFn(id, node);
|
||||
iceList.appendChild(node);
|
||||
});
|
||||
|
||||
imgLoader.reload();
|
||||
}
|
||||
|
||||
function showICEList() {
|
||||
MainNavigation.go('ice-view', 'right-left');
|
||||
iceListDisplayed = true;
|
||||
}
|
||||
|
||||
function hideICEList() {
|
||||
MainNavigation.back();
|
||||
iceListDisplayed = false;
|
||||
}
|
||||
|
||||
var ICEView = {
|
||||
init: init,
|
||||
showICEList: showICEList,
|
||||
hideICEList: hideICEList,
|
||||
get iceListDisplayed() {
|
||||
return iceListDisplayed;
|
||||
}
|
||||
};
|
||||
|
||||
exports.ICEView = ICEView;
|
||||
|
||||
})(window);
|
||||
354
gaia/apps/communications/contacts/js/views/ice_settings.js
Executable file
354
gaia/apps/communications/contacts/js/views/ice_settings.js
Executable file
|
|
@ -0,0 +1,354 @@
|
|||
/* global Contacts */
|
||||
/* global ICEData */
|
||||
/* global ConfirmDialog */
|
||||
/* global ContactsService */
|
||||
/* global HeaderUI */
|
||||
/* global Search */
|
||||
|
||||
/**
|
||||
* ICE Settings view. In charge of selecting
|
||||
* the contacts for emergency and keeping sync
|
||||
* the datastore to share those contacts with
|
||||
* dialer and lockscreen.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var contacts = window.contacts || {};
|
||||
|
||||
(function(exports) {
|
||||
var iceSettingsPanel,
|
||||
iceSettingsHeader,
|
||||
iceContactItems = [],
|
||||
iceContactCheckboxes = [],
|
||||
iceContactButtons = [],
|
||||
iceScreenInitialized = false,
|
||||
currentICETarget;
|
||||
|
||||
/**
|
||||
* Redraws the ICE contact list. This is not an expensive
|
||||
* operation since we have a maximun of 2 contacts.
|
||||
* The first time executed will attach listeners for the
|
||||
* frame ui.
|
||||
*/
|
||||
var init = function ice_init() {
|
||||
if (iceScreenInitialized) {
|
||||
return;
|
||||
}
|
||||
// ICE DOM elements
|
||||
iceSettingsPanel = document.getElementById('ice-settings');
|
||||
iceSettingsHeader = document.getElementById('ice-settings-header');
|
||||
|
||||
iceContactItems.push(document.getElementById('ice-contacts-1-switch'));
|
||||
iceContactItems.push(document.getElementById('ice-contacts-2-switch'));
|
||||
|
||||
iceContactCheckboxes.push(iceContactItems[0]
|
||||
.querySelector('[name="ice-contact-1-enabled"]'));
|
||||
iceContactCheckboxes.push(iceContactItems[1]
|
||||
.querySelector('[name="ice-contact-2-enabled"]'));
|
||||
iceContactButtons.push(document.getElementById('select-ice-contact-1'));
|
||||
iceContactButtons.push(document.getElementById('select-ice-contact-2'));
|
||||
|
||||
iceContactButtons[0].dataset.contactId = '';
|
||||
iceContactButtons[1].dataset.contactId = '';
|
||||
|
||||
// ICE Events handlers
|
||||
iceSettingsHeader.addEventListener('action', function(){
|
||||
contacts.Settings.navigation.back();
|
||||
});
|
||||
|
||||
// All the controls do the same, just modifications on the
|
||||
// specific order.
|
||||
iceContactItems.forEach(function(item, index) {
|
||||
iceContactCheckboxes[index].addEventListener('change', function(i) {
|
||||
return function(evt) {
|
||||
var localIceContacts = ICEData.iceContacts;
|
||||
var wasActive = !iceContactCheckboxes[i].checked;
|
||||
|
||||
if (wasActive) {
|
||||
resetIceGroupState(i);
|
||||
if (localIceContacts[i] && localIceContacts[i].id) {
|
||||
setICEContact(null, i, !wasActive);
|
||||
}
|
||||
}
|
||||
else {
|
||||
iceContactButtons[i].disabled = false;
|
||||
}
|
||||
};
|
||||
}(index));
|
||||
});
|
||||
|
||||
iceContactButtons.forEach(function(element){
|
||||
element.addEventListener('click', function(evt) {
|
||||
showSelectList(evt.target.id);
|
||||
});
|
||||
// Bug 1071064 If users clicked on disabled button, the switch was checked
|
||||
element.parentNode.addEventListener('click', (evt) => {
|
||||
evt.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
iceScreenInitialized = true;
|
||||
};
|
||||
|
||||
function reloadButtonsState(cb) {
|
||||
ICEData.load().then(retrieveContactData.bind(null, cb));
|
||||
}
|
||||
|
||||
function retrieveContactData(cb) {
|
||||
var iceContactsData = [];
|
||||
var iceContactsIds = ICEData.iceContacts;
|
||||
var numRetrievedContacts = 0;
|
||||
|
||||
iceContactsIds.forEach(function(iceContact, index) {
|
||||
ContactsService.get(iceContact.id, function(cindex, contact) {
|
||||
var theContact = {
|
||||
active: iceContactsIds[cindex].active,
|
||||
mozContact: contact
|
||||
};
|
||||
|
||||
iceContactsData[cindex] = theContact;
|
||||
|
||||
numRetrievedContacts++;
|
||||
|
||||
if (numRetrievedContacts === 2) {
|
||||
setButtonsState(iceContactsData, cb);
|
||||
}
|
||||
}.bind(null, index));
|
||||
});
|
||||
}
|
||||
|
||||
function refresh(done) {
|
||||
if (!iceScreenInitialized) {
|
||||
init();
|
||||
}
|
||||
reloadButtonsState(done);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an object representing the internal state
|
||||
* fills the UI elements.
|
||||
* @params iceContactsIds (Array) list of contacts and state
|
||||
*/
|
||||
function setButtonsState(iceContactsData, done) {
|
||||
iceContactsData.forEach(setIceButtonState);
|
||||
|
||||
typeof done === 'function' && done();
|
||||
}
|
||||
|
||||
function setIceButtonState(iceContactData, index) {
|
||||
if (!iceContactData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (iceContactData.mozContact) {
|
||||
var iceContact = iceContactData.mozContact;
|
||||
|
||||
var givenName = (Array.isArray(iceContact.givenName) &&
|
||||
iceContact.givenName[0]) || '';
|
||||
var familyName = (Array.isArray(iceContact.familyName) &&
|
||||
iceContact.familyName[0]) || '';
|
||||
|
||||
var display = [givenName, familyName];
|
||||
var iceLabel = display.join(' ').trim();
|
||||
// If contact has no name we the first tel number will be used
|
||||
if (!iceLabel) {
|
||||
if (Array.isArray(iceContact.tel) && iceContact.tel[0]) {
|
||||
iceLabel = iceContact.tel[0].value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
buildIceContactUI(index, iceLabel, iceContact.id,
|
||||
iceContactData.active);
|
||||
}
|
||||
else {
|
||||
resetIceGroupState(index);
|
||||
}
|
||||
}
|
||||
|
||||
function buildIceContactUI(index, label, contactId, active) {
|
||||
iceContactCheckboxes[index].checked = active;
|
||||
iceContactButtons[index].disabled = !active;
|
||||
|
||||
var bdi = document.createElement('bdi');
|
||||
bdi.classList.add('ice-contact');
|
||||
bdi.textContent = label;
|
||||
iceContactButtons[index].innerHTML = '';
|
||||
iceContactButtons[index].appendChild(bdi);
|
||||
iceContactButtons[index].dataset.contactId = contactId;
|
||||
}
|
||||
|
||||
function resetIceGroupState(index) {
|
||||
iceContactCheckboxes[index].checked = false;
|
||||
|
||||
iceContactButtons[index].disabled = true;
|
||||
iceContactButtons[index].innerHTML = '';
|
||||
iceContactButtons[index].dataset.contactId = '';
|
||||
iceContactButtons[index].setAttribute('data-l10n-id', 'ICESelectContact');
|
||||
}
|
||||
|
||||
function resetIceGroupStates() {
|
||||
for(var j = 0; j < iceContactCheckboxes.length; j++) {
|
||||
resetIceGroupState(j);
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
contacts.List.clearClickHandlers();
|
||||
contacts.List.handleClick(Contacts.showContactDetail);
|
||||
HeaderUI.setNormalHeader();
|
||||
|
||||
var hasICESet = ICEData.iceContacts.find(function(x) {
|
||||
return x.active === true;
|
||||
});
|
||||
|
||||
if (!hasICESet) {
|
||||
resetIceGroupStates();
|
||||
}
|
||||
|
||||
if (window.Search && Search.isInSearchMode()) {
|
||||
Search.exitSearchMode();
|
||||
}
|
||||
|
||||
contacts.Settings.navigation.back(() => {
|
||||
hasICESet && contacts.List.toggleICEGroup(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a contact id, saves it internally. Also restores the contact
|
||||
* list default handler.
|
||||
* In case of not valid contact it will display a message on the screen
|
||||
* indicating the cause of the error.
|
||||
* @param id (string) contact id
|
||||
*/
|
||||
function selectICEHandler(id) {
|
||||
checkContact(id).then(function() {
|
||||
setICEContact(id, currentICETarget, true, goBack);
|
||||
}, function error(l10nId) {
|
||||
var dismiss = {
|
||||
title: 'ok',
|
||||
callback: function() {
|
||||
ConfirmDialog.hide();
|
||||
}
|
||||
};
|
||||
ConfirmDialog.show(null, l10nId || 'ICEUnknownError', dismiss);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Will perform a series of checks to validate the selected
|
||||
* contact as a valid ICE contact
|
||||
* @param id (string) contact id
|
||||
* @return (Promise) fulfilled if contact is valid
|
||||
*/
|
||||
function checkContact(id) {
|
||||
return ICEData.load().then(function() {
|
||||
return contactNotICE(id).then(contactNotAllowed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a contacts is already set as ICE
|
||||
* @param id (string) contact id
|
||||
* @return (Promise) fulfilled if contact is not repeated,
|
||||
* rejected otherwise
|
||||
*/
|
||||
function contactNotICE(id) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var isICE = ICEData.iceContacts.some(function(x) {
|
||||
return x.id === id;
|
||||
});
|
||||
|
||||
if (isICE) {
|
||||
reject('ICERepeatedContact');
|
||||
} else {
|
||||
resolve(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to avoid selecting contacts as ICE if they don't have a phone number
|
||||
* or they are a Facebook contact
|
||||
* @param id (String) contact id
|
||||
* @returns (Promise) Fulfilled if contact has phone
|
||||
*/
|
||||
function contactNotAllowed(id) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
ContactsService.get(id, function(contact) {
|
||||
if(Array.isArray(contact.tel) && contact.tel[0] &&
|
||||
contact.tel[0].value && contact.tel[0].value.trim()) {
|
||||
resolve(id);
|
||||
} else {
|
||||
reject('ICEContactNoNumber');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* We are using the contact list to choose a contact as ICE. For doing
|
||||
* this, we need to setup our own click handler.
|
||||
* @para target (HTMLButton) Button click to select an ICE contact
|
||||
*/
|
||||
function showSelectList(target) {
|
||||
contacts.List.toggleICEGroup(false);
|
||||
HeaderUI.setCancelableHeader(goBack, 'selectContact');
|
||||
contacts.Settings.navigation.go('view-contacts-list', 'right-left');
|
||||
currentICETarget = target === 'select-ice-contact-1' ? 0 : 1;
|
||||
contacts.List.clearClickHandlers();
|
||||
contacts.List.handleClick(selectICEHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values for ICE contacts, both in local and in the
|
||||
* datastore
|
||||
* @param id (string) contact id
|
||||
* @param pos (int) current position (0,1)
|
||||
* @param active (boolean) ice contact is active or not
|
||||
* @param cb (function) callback when ready
|
||||
*/
|
||||
function setICEContact(id, pos, active, cb) {
|
||||
ICEData.setICEContact(id, pos, active).then(function() {
|
||||
// Only reload contact info in case there is a change in the contact
|
||||
if (id === iceContactButtons[pos].dataset.contactId) {
|
||||
return;
|
||||
}
|
||||
|
||||
ContactsService.get(id, function(contact) {
|
||||
var theContact = {
|
||||
active: active,
|
||||
mozContact: contact
|
||||
};
|
||||
var iceContactData = [];
|
||||
if (pos === 0) {
|
||||
iceContactData[1] = null;
|
||||
}
|
||||
else {
|
||||
iceContactData[0] = null;
|
||||
}
|
||||
iceContactData[pos] = theContact;
|
||||
|
||||
setButtonsState(iceContactData);
|
||||
|
||||
typeof cb === 'function' && cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reset() {
|
||||
iceScreenInitialized = false;
|
||||
iceContactItems = [];
|
||||
iceContactCheckboxes = [];
|
||||
iceContactButtons = [];
|
||||
currentICETarget = null;
|
||||
}
|
||||
|
||||
exports.ICE = {
|
||||
init: init,
|
||||
refresh: refresh,
|
||||
reset: reset,
|
||||
get initialized() { return iceScreenInitialized; }
|
||||
};
|
||||
})(window);
|
||||
2443
gaia/apps/communications/contacts/js/views/list.js
Executable file
2443
gaia/apps/communications/contacts/js/views/list.js
Executable file
File diff suppressed because it is too large
Load diff
148
gaia/apps/communications/contacts/js/views/multiple_select.js
Executable file
148
gaia/apps/communications/contacts/js/views/multiple_select.js
Executable file
|
|
@ -0,0 +1,148 @@
|
|||
/* global Contacts, contacts, utils, LazyLoader */
|
||||
/* global ContactsService */
|
||||
/* global Matcher */
|
||||
|
||||
'use strict';
|
||||
|
||||
window.Contacts = window.Contacts || {};
|
||||
|
||||
Contacts.MultipleSelect = (function() {
|
||||
const STATUS_TIMEOUT = 3000; // ms before closing view which shows status msg
|
||||
var contactsToImport = [];
|
||||
var header, saveButton, container, template, title;
|
||||
|
||||
/**
|
||||
* Initializes references to DOM elements. This is needed before calling
|
||||
* render.
|
||||
*/
|
||||
function init() {
|
||||
header = document.getElementById('multiple-select-view-header');
|
||||
saveButton = document.getElementById('save-button');
|
||||
container = document.getElementById('multiple-select-container');
|
||||
template = document.getElementById('contactToImportTemplate');
|
||||
title = document.getElementById('multiple-select-view-title');
|
||||
|
||||
saveButton.addEventListener('click', importAll);
|
||||
header.addEventListener('action', cancelHandler);
|
||||
}
|
||||
|
||||
function cancelHandler() {
|
||||
header.removeEventListener('action', cancelHandler);
|
||||
saveButton.removeEventListener('click', importAll);
|
||||
Contacts.cancel();
|
||||
}
|
||||
|
||||
function importAll() {
|
||||
var numDupsMerged = 0,
|
||||
importedContacts = 0,
|
||||
parsedContacts = 0;
|
||||
const DEPENDENCIES = [
|
||||
'/shared/js/contacts/import/utilities/status.js',
|
||||
'/shared/js/simple_phone_matcher.js',
|
||||
'/shared/js/contacts/contacts_matcher.js',
|
||||
'/shared/js/contacts/contacts_merger.js',
|
||||
'/shared/js/contacts/merger_adapter.js'
|
||||
];
|
||||
LazyLoader.load(DEPENDENCIES, function() {
|
||||
contactsToImport.forEach((contact, index) => {
|
||||
Matcher.match(contact, 'passive', {
|
||||
onmatch: (matches) => {
|
||||
var callbacks = {
|
||||
success: () => {
|
||||
numDupsMerged++;
|
||||
doContinue(true);
|
||||
},
|
||||
error: doContinue
|
||||
};
|
||||
contacts.adaptAndMerge(contact, matches, callbacks);
|
||||
},
|
||||
onmismatch: () => {
|
||||
|
||||
ContactsService.save(
|
||||
contact,
|
||||
function(e) {
|
||||
if (e) {
|
||||
console.error(e);
|
||||
doContinue();
|
||||
return;
|
||||
}
|
||||
doContinue(true);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function doContinue(isContactImported) {
|
||||
parsedContacts++;
|
||||
if (isContactImported) {
|
||||
importedContacts++;
|
||||
}
|
||||
|
||||
if (contactsToImport.length <= parsedContacts) {
|
||||
utils.status.show({
|
||||
id: 'vCardContacts-imported',
|
||||
args: {
|
||||
n: importedContacts
|
||||
}
|
||||
}, {
|
||||
id: 'contactsMerged',
|
||||
args: {
|
||||
numDups: numDupsMerged
|
||||
}
|
||||
});
|
||||
|
||||
window.setTimeout(cancelHandler, STATUS_TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to preview which contacts will be imported. Requires to call init
|
||||
* first when DOM is ready.
|
||||
*
|
||||
* @param {mozContact[]} contacts - Every contact already read from vCard.
|
||||
* @param cursor - Reader object to continue reading from vCard.
|
||||
* @param {string} filename - Name of the vcf file we are reading from.
|
||||
*/
|
||||
var render = function(contacts, cursor, filename) {
|
||||
container.innerHTML = ''; // Avoids rendering the same content twice.
|
||||
title.textContent = filename;
|
||||
contacts.forEach(doRenderContact);
|
||||
|
||||
cursor.onsuccess = function(event) {
|
||||
var contact = event.target.result;
|
||||
if (contact) {
|
||||
doRenderContact(contact);
|
||||
cursor.continue();
|
||||
}
|
||||
};
|
||||
|
||||
function doRenderContact(contact) {
|
||||
var clone = document.importNode(template.content, true);
|
||||
|
||||
var contactData = clone.querySelectorAll('p');
|
||||
contactData[0].textContent =
|
||||
Array.isArray(contact.name) && contact.name[0];
|
||||
var org = (Array.isArray(contact.org) && contact.org[0]) ?
|
||||
contact.org[0] : '';
|
||||
contactData[1].textContent = org;
|
||||
|
||||
if (Array.isArray(contact.photo) && contact.photo[0] instanceof Blob) {
|
||||
var picture = clone.querySelector('aside span');
|
||||
picture.style.backgroundImage = 'url(' +
|
||||
window.URL.createObjectURL(contact.photo[0]) +
|
||||
')';
|
||||
}
|
||||
|
||||
contactsToImport.push(contact);
|
||||
container.appendChild(clone);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
render: render,
|
||||
init: init
|
||||
};
|
||||
})();
|
||||
856
gaia/apps/communications/contacts/js/views/settings.js
Executable file
856
gaia/apps/communications/contacts/js/views/settings.js
Executable file
|
|
@ -0,0 +1,856 @@
|
|||
'use strict';
|
||||
/* global _ */
|
||||
/* global Cache */
|
||||
/* global ConfirmDialog */
|
||||
/* global Contacts */
|
||||
/* global ContactsBTExport */
|
||||
/* global ContactsExporter */
|
||||
/* global ContactsSDExport */
|
||||
/* global ContactsSIMExport */
|
||||
/* global IccHandler */
|
||||
/* global LazyLoader */
|
||||
/* global navigationStack */
|
||||
/* global Rest */
|
||||
/* global SimContactsImporter */
|
||||
/* global SimDomGenerator */
|
||||
/* global utils */
|
||||
/* global VCFReader */
|
||||
/* global ContactsService */
|
||||
/* global ExtServices */
|
||||
/* global Loader */
|
||||
/* global BulkDelete */
|
||||
/* global ICE */
|
||||
/* global Telemetry */
|
||||
|
||||
var contacts = window.contacts || {};
|
||||
|
||||
/***
|
||||
This class handles all the activity regarding
|
||||
the settings screen for contacts
|
||||
**/
|
||||
contacts.Settings = (function() {
|
||||
|
||||
var navigationHandler,
|
||||
importSettingsHeader,
|
||||
orderCheckBox,
|
||||
orderItem,
|
||||
orderByLastName,
|
||||
setICEButton,
|
||||
importSettingsPanel,
|
||||
importSettingsTitle,
|
||||
importContacts,
|
||||
exportContacts,
|
||||
importOptions,
|
||||
exportOptions,
|
||||
importLiveOption,
|
||||
importGmailOption,
|
||||
importSDOption,
|
||||
exportSDOption,
|
||||
newOrderByLastName = null,
|
||||
PENDING_LOGOUT_KEY = 'pendingLogout',
|
||||
bulkDeleteButton;
|
||||
|
||||
var EXPORT_TRANSITION_LEVEL = 2, DELETE_TRANSITION_LEVEL = 1;
|
||||
|
||||
// Initialise the settings screen (components, listeners ...)
|
||||
var init = function initialize() {
|
||||
// Create the DOM for our SIM cards and listen to any changes
|
||||
IccHandler.init(new SimDomGenerator(), contacts.Settings.cardStateChanged);
|
||||
|
||||
initContainers();
|
||||
// To avoid any race condition we listen for online events once
|
||||
// containers have been initialized
|
||||
window.addEventListener('online', checkOnline);
|
||||
window.addEventListener('offline', checkOnline);
|
||||
|
||||
// Subscribe to events related to change state in the sd card
|
||||
utils.sdcard.subscribeToChanges('check_sdcard', function(value) {
|
||||
updateStorageOptions(utils.sdcard.checkStorageCard());
|
||||
});
|
||||
|
||||
window.addEventListener('timeformatchange', updateTimestamps);
|
||||
};
|
||||
|
||||
var hideSettings = function hideSettings() {
|
||||
contacts.Settings.close();
|
||||
};
|
||||
|
||||
// Get the different values that we will show in the app
|
||||
var getData = function getData() {
|
||||
var config = utils.cookie.load();
|
||||
var order = config ? config.order : false;
|
||||
orderByLastName = order;
|
||||
newOrderByLastName = null;
|
||||
updateOrderingUI();
|
||||
};
|
||||
|
||||
var updateOrderingUI = function updateOrderingUI() {
|
||||
var value = newOrderByLastName === null ? orderByLastName :
|
||||
newOrderByLastName;
|
||||
orderCheckBox.checked = value;
|
||||
orderCheckBox.setAttribute('aria-checked', value);
|
||||
};
|
||||
|
||||
var updateImportTitle = function updateImportTitle(l10nString) {
|
||||
importSettingsTitle.setAttribute('data-l10n-id', l10nString);
|
||||
};
|
||||
|
||||
// Initialises variables and listener for the UI
|
||||
var initContainers = function initContainers() {
|
||||
orderItem = document.getElementById('settingsOrder');
|
||||
orderCheckBox = orderItem.querySelector('[name="order.lastname"]');
|
||||
orderCheckBox.addEventListener('change', onOrderingChange.bind(this));
|
||||
// Creating a navigation handler from this view
|
||||
navigationHandler = new navigationStack('view-settings');
|
||||
|
||||
// Init panel & elements for caching them
|
||||
importSettingsPanel = document.getElementById('import-settings');
|
||||
importSDOption = document.getElementById('import-sd-option');
|
||||
exportSDOption = document.getElementById('export-sd-option');
|
||||
importSettingsTitle = document.getElementById('import-settings-title');
|
||||
importLiveOption = document.getElementById('import-live-option');
|
||||
importGmailOption = document.getElementById('import-gmail-option');
|
||||
|
||||
// ICE view
|
||||
setICEButton = document.getElementById('set-ice');
|
||||
|
||||
/*
|
||||
* Adding listeners
|
||||
*/
|
||||
|
||||
document.getElementById('settings-close').addEventListener(
|
||||
'click',
|
||||
hideSettings
|
||||
);
|
||||
// Listener for updating the timestamp based on extServices
|
||||
window.addEventListener('message', function updateList(e) {
|
||||
if (e.data.type === 'import_updated') {
|
||||
updateTimestamps();
|
||||
checkNoContacts();
|
||||
}
|
||||
});
|
||||
|
||||
// Navigation back
|
||||
importSettingsHeader = document.getElementById('import-settings-header');
|
||||
importSettingsHeader.addEventListener('action', importSettingsBackHandler);
|
||||
|
||||
// Handlers for the navigation through the panels
|
||||
importContacts = document.getElementById('importContacts');
|
||||
importContacts.firstElementChild.
|
||||
addEventListener('click', importContactsHandler);
|
||||
|
||||
exportContacts = document.getElementById('exportContacts');
|
||||
exportContacts.firstElementChild.
|
||||
addEventListener('click', exportContactsHandler);
|
||||
|
||||
// Handlers for the actions related with EXPORT/IMPORT
|
||||
importOptions = document.getElementById('import-options');
|
||||
importOptions.addEventListener('click', importOptionsHandler);
|
||||
|
||||
exportOptions = document.getElementById('export-options');
|
||||
exportOptions.addEventListener('click', exportOptionsHandler);
|
||||
|
||||
// ICE view
|
||||
setICEButton.addEventListener('click', showICEScreen);
|
||||
|
||||
// Bulk delete
|
||||
bulkDeleteButton = document.getElementById('bulkDelete');
|
||||
bulkDeleteButton.addEventListener('click', bulkDeleteHandler);
|
||||
};
|
||||
|
||||
// UI event handlers
|
||||
function importSettingsBackHandler() {
|
||||
navigationHandler.back(function navigateBackHandler() {
|
||||
// Removing the previous assigned style for having
|
||||
// a clean view
|
||||
importSettingsPanel.classList.remove('export');
|
||||
importSettingsPanel.classList.remove('import');
|
||||
});
|
||||
}
|
||||
|
||||
function importContactsHandler() {
|
||||
// Hide elements for export and transition
|
||||
LazyLoader.load(['/contacts/js/utilities/telemetry.js'], () => {
|
||||
importSettingsPanel.classList.remove('export');
|
||||
importSettingsPanel.classList.add('import');
|
||||
updateImportTitle('importContactsTitle');
|
||||
navigationHandler.go('import-settings', 'right-left');
|
||||
});
|
||||
}
|
||||
|
||||
function exportContactsHandler() {
|
||||
// Hide elements for import and transition
|
||||
LazyLoader.load(['/contacts/js/export/contacts_exporter.js'], loadSearch);
|
||||
|
||||
function loadSearch() {
|
||||
Loader.view('search', function() {
|
||||
importSettingsPanel.classList.remove('import');
|
||||
importSettingsPanel.classList.add('export');
|
||||
updateImportTitle('exportContactsTitle');
|
||||
navigationHandler.go('import-settings', 'right-left');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showICEScreen(cb) {
|
||||
LazyLoader.load([
|
||||
'/contacts/js/utilities/ice_data.js',
|
||||
'/contacts/js/views/ice_settings.js',
|
||||
'/shared/js/contacts/utilities/ice_store.js'], function(){
|
||||
ICE.refresh();
|
||||
navigationHandler.go('ice-settings', 'right-left');
|
||||
if (typeof cb === 'function') {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Given an event, select wich should be the targeted
|
||||
// import/export source
|
||||
function getSource(e) {
|
||||
var source = e.target.parentNode.dataset.source;
|
||||
// Check special cases
|
||||
if (source && source.indexOf('-') != -1) {
|
||||
source = source.substr(0, source.indexOf('-'));
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
function logImportUsage(name) {
|
||||
return Telemetry.logImportUsage(name);
|
||||
}
|
||||
|
||||
function importOptionsHandler(e) {
|
||||
/* jshint validthis:true */
|
||||
|
||||
var source = getSource(e);
|
||||
switch (source) {
|
||||
case 'sim':
|
||||
var iccId = e.target.parentNode.dataset.iccid;
|
||||
window.setTimeout(requireSimImport.bind(this,
|
||||
onSimImport.bind(this, iccId)), 0);
|
||||
break;
|
||||
case 'sd':
|
||||
window.setTimeout(requireOverlay.bind(this, onSdImport), 0);
|
||||
break;
|
||||
case 'gmail':
|
||||
ExtServices.onContactsImported = logImportUsage.bind(null, 'gmail');
|
||||
ExtServices.importGmail();
|
||||
break;
|
||||
case 'live':
|
||||
ExtServices.onContactsImported = logImportUsage.bind(null, 'live');
|
||||
ExtServices.importLive();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var bulkDeleteHandler = function bulkDeleteHandler() {
|
||||
LazyLoader.load(
|
||||
[
|
||||
'/contacts/js/contacts_bulk_delete.js',
|
||||
'/contacts/js/contacts_remover.js'
|
||||
],
|
||||
function() {
|
||||
Loader.view('search', function() {
|
||||
contacts.List.selectFromList(_('DeleteTitle'),
|
||||
function onSelectedContacts(promise, done) {
|
||||
BulkDelete.performDelete(promise, done);
|
||||
},
|
||||
null,
|
||||
navigationHandler,
|
||||
{
|
||||
transitionLevel: DELETE_TRANSITION_LEVEL
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function exportOptionsHandler(e) {
|
||||
var source = getSource(e);
|
||||
switch (source) {
|
||||
case 'sim':
|
||||
var iccId = e.target.parentNode.dataset.iccid;
|
||||
LazyLoader.load(['/contacts/js/export/sim.js'],
|
||||
function() {
|
||||
doExport(new ContactsSIMExport(IccHandler.getIccById(iccId)));
|
||||
}
|
||||
);
|
||||
break;
|
||||
case 'sd':
|
||||
LazyLoader.load(
|
||||
[
|
||||
'/shared/js/device_storage/get_storage_if_available.js',
|
||||
'/shared/js/device_storage/get_unused_filename.js',
|
||||
'/shared/js/contact2vcard.js',
|
||||
'/shared/js/setImmediate.js',
|
||||
'/contacts/js/export/sd.js'
|
||||
],
|
||||
function() {
|
||||
doExport(new ContactsSDExport());
|
||||
}
|
||||
);
|
||||
break;
|
||||
case 'bluetooth':
|
||||
LazyLoader.load(
|
||||
[
|
||||
'/shared/js/device_storage/get_storage_if_available.js',
|
||||
'/shared/js/device_storage/get_unused_filename.js',
|
||||
'/shared/js/contact2vcard.js',
|
||||
'/shared/js/setImmediate.js',
|
||||
'/contacts/js/export/bt.js'
|
||||
],
|
||||
function() {
|
||||
doExport(new ContactsBTExport());
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function doExport(strategy) {
|
||||
// Launch the selection mode in the list, and then invoke
|
||||
// the export with the selected strategy.
|
||||
|
||||
contacts.List.selectFromList(_('exportContactsAction'),
|
||||
function onSelectedContacts(promise) {
|
||||
// Resolve the promise, meanwhile show an overlay to
|
||||
// warn the user of the ongoin operation, dismiss it
|
||||
// once we have the result
|
||||
requireOverlay(function _loaded() {
|
||||
utils.overlay.show('preparing-contacts', null, 'spinner');
|
||||
promise.onsuccess = function onSuccess(ids) {
|
||||
// Once we start the export process we can exit from select mode
|
||||
// This will have to evolve once export errors can be captured
|
||||
contacts.List.exitSelectMode();
|
||||
var exporter = new ContactsExporter(strategy);
|
||||
exporter.init(ids, function onExporterReady() {
|
||||
// Leave the contact exporter to deal with the overlay
|
||||
exporter.start();
|
||||
});
|
||||
};
|
||||
promise.onerror = function onError() {
|
||||
contacts.List.exitSelectMode();
|
||||
utils.overlay.hide();
|
||||
};
|
||||
});
|
||||
},
|
||||
null,
|
||||
navigationHandler,
|
||||
{
|
||||
isDanger: false,
|
||||
transitionLevel: EXPORT_TRANSITION_LEVEL
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Options checking & updating
|
||||
|
||||
var checkSIMCard = function checkSIMCard() {
|
||||
var statuses = IccHandler.getStatus();
|
||||
statuses.forEach(function onStatus(status) {
|
||||
enableSIMOptions(status.iccId, status.cardState);
|
||||
});
|
||||
};
|
||||
|
||||
// Disables/Enables an option and show the error if needed
|
||||
var updateOptionStatus =
|
||||
function updateOptionStatus(domOption, disabled, error) {
|
||||
if (domOption === null) {
|
||||
return;
|
||||
}
|
||||
var optionButton = domOption.firstElementChild;
|
||||
if (disabled) {
|
||||
optionButton.setAttribute('disabled', 'disabled');
|
||||
if (error) {
|
||||
domOption.classList.add('error');
|
||||
} else {
|
||||
domOption.classList.remove('error');
|
||||
}
|
||||
} else {
|
||||
optionButton.removeAttribute('disabled');
|
||||
domOption.classList.remove('error');
|
||||
}
|
||||
};
|
||||
|
||||
// Disables/Enables the actions over the sim import functionality
|
||||
var enableSIMOptions = function enableSIMOptions(iccId, cardState) {
|
||||
var importSimOption = document.getElementById('import-sim-option-' + iccId);
|
||||
var exportSimOption = document.getElementById('export-sim-option-' + iccId);
|
||||
var disabled = (cardState !== 'ready' && cardState !== 'illegal');
|
||||
updateOptionStatus(importSimOption, disabled, true);
|
||||
updateOptionStatus(exportSimOption, disabled, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Disables/Enables the actions over the sdcard import/export functionality
|
||||
* @param {Boolean} cardAvailable Whether functions should be enabled or not.
|
||||
*/
|
||||
var updateStorageOptions = function updateStorageOptions(cardAvailable) {
|
||||
// Enable/Disable button and shows/hides error message
|
||||
updateOptionStatus(importSDOption, !cardAvailable, true);
|
||||
updateOptionStatus(exportSDOption, !cardAvailable, true);
|
||||
|
||||
var importSDErrorL10nId = null;
|
||||
var exportSDErrorL10nId = null;
|
||||
|
||||
var cardShared = utils.sdcard.status === utils.sdcard.SHARED;
|
||||
if (!cardAvailable) {
|
||||
importSDErrorL10nId = 'noMemoryCardMsg';
|
||||
exportSDErrorL10nId = 'noMemoryCardMsgExport';
|
||||
|
||||
if (cardShared) {
|
||||
importSDErrorL10nId = exportSDErrorL10nId = 'memoryCardUMSEnabled';
|
||||
}
|
||||
}
|
||||
|
||||
// update the message
|
||||
var importSDErrorNode = importSDOption.querySelector('p.error-message');
|
||||
if (importSDErrorL10nId) {
|
||||
importSDErrorNode.setAttribute('data-l10n-id', importSDErrorL10nId);
|
||||
} else {
|
||||
importSDErrorNode.removeAttribute('data-l10n-id');
|
||||
importSDErrorNode.textContent = '';
|
||||
}
|
||||
|
||||
var exportSDErrorNode = exportSDOption.querySelector('p');
|
||||
if (exportSDErrorL10nId) {
|
||||
exportSDErrorNode.setAttribute('data-l10n-id', exportSDErrorL10nId);
|
||||
} else {
|
||||
exportSDErrorNode.removeAttribute('data-l10n-id');
|
||||
exportSDErrorNode.textContent = '';
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads the overlay class before showing
|
||||
*/
|
||||
function requireOverlay(callback) {
|
||||
Loader.utility('Overlay', callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads required libraries for sim import
|
||||
*/
|
||||
function requireSimImport(callback) {
|
||||
var libraries = ['Overlay', 'Import_sim_contacts'];
|
||||
var pending = libraries.length;
|
||||
|
||||
libraries.forEach(function onPending(library) {
|
||||
Loader.utility(library, next);
|
||||
});
|
||||
|
||||
function next() {
|
||||
if (!(--pending)) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetWait(wakeLock) {
|
||||
Contacts.hideOverlay();
|
||||
if (wakeLock) {
|
||||
wakeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
// Listens for any change in the ordering preferences
|
||||
var onOrderingChange = function onOrderingChange(evt) {
|
||||
newOrderByLastName = orderCheckBox.checked;
|
||||
updateOrderingUI();
|
||||
};
|
||||
|
||||
// Import contacts from SIM card and updates ui
|
||||
var onSimImport = function onSimImport(iccId, done) {
|
||||
var icc = IccHandler.getIccById(iccId);
|
||||
if (icc === null) {
|
||||
return;
|
||||
}
|
||||
var progress = Contacts.showOverlay('simContacts-reading',
|
||||
'activityBar');
|
||||
|
||||
var wakeLock = navigator.requestWakeLock('cpu');
|
||||
|
||||
var cancelled = false, contactsRead = false;
|
||||
var importer = new SimContactsImporter(icc);
|
||||
utils.overlay.showMenu();
|
||||
utils.overlay.oncancel = function oncancel() {
|
||||
cancelled = true;
|
||||
importer.finish();
|
||||
if (contactsRead) {
|
||||
// A message about canceling will be displayed while the current chunk
|
||||
// is being cooked
|
||||
progress.setClass('activityBar');
|
||||
utils.overlay.hideMenu();
|
||||
progress.setHeaderMsg('messageCanceling');
|
||||
} else {
|
||||
importer.onfinish(); // Early return while reading contacts
|
||||
}
|
||||
};
|
||||
var totalContactsToImport;
|
||||
var importedContacts = 0;
|
||||
// Delay for showing feedback to the user after importing
|
||||
var DELAY_FEEDBACK = 200;
|
||||
|
||||
importer.onread = function import_read(n) {
|
||||
contactsRead = true;
|
||||
totalContactsToImport = n;
|
||||
if (totalContactsToImport > 0) {
|
||||
progress.setClass('progressBar');
|
||||
progress.setHeaderMsg('simContacts-importing');
|
||||
progress.setTotal(totalContactsToImport);
|
||||
}
|
||||
};
|
||||
|
||||
importer.onfinish = function import_finish(numDupsMerged) {
|
||||
window.setTimeout(function onfinish_import() {
|
||||
resetWait(wakeLock);
|
||||
if (importedContacts > 0) {
|
||||
var source = 'sim-' + iccId;
|
||||
utils.misc.setTimestamp(source, function() {
|
||||
// Once the timestamp is saved, update the list
|
||||
updateTimestamps();
|
||||
checkNoContacts();
|
||||
});
|
||||
}
|
||||
if (!cancelled) {
|
||||
utils.status.show({
|
||||
id: 'simContacts-imported3',
|
||||
args: {
|
||||
n: importedContacts
|
||||
}
|
||||
},
|
||||
!numDupsMerged ? null : {
|
||||
id: 'contactsMerged',
|
||||
args: {
|
||||
numDups: numDupsMerged
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logImportUsage('sim');
|
||||
}
|
||||
|
||||
typeof done === 'function' && done();
|
||||
|
||||
}, DELAY_FEEDBACK);
|
||||
|
||||
importer.onfinish = null;
|
||||
};
|
||||
|
||||
importer.onimported = function imported_contact() {
|
||||
importedContacts++;
|
||||
if (!cancelled) {
|
||||
progress.update();
|
||||
}
|
||||
};
|
||||
|
||||
importer.onerror = function import_error() {
|
||||
var cancel = {
|
||||
title: 'cancel',
|
||||
callback: function() {
|
||||
ConfirmDialog.hide();
|
||||
}
|
||||
};
|
||||
var retry = {
|
||||
title: 'retry',
|
||||
isRecommend: true,
|
||||
callback: function() {
|
||||
ConfirmDialog.hide();
|
||||
// And now the action is reproduced one more time
|
||||
window.setTimeout(requireSimImport.bind(this,
|
||||
onSimImport.bind(this, iccId)), 0);
|
||||
}
|
||||
};
|
||||
ConfirmDialog.show(null, 'simContacts-error', cancel, retry);
|
||||
resetWait(wakeLock);
|
||||
};
|
||||
|
||||
importer.start();
|
||||
};
|
||||
|
||||
var onSdImport = function onSdImport(cb) {
|
||||
var cancelled = false;
|
||||
var importer = null;
|
||||
var progress = Contacts.showOverlay(
|
||||
'memoryCardContacts-importing',
|
||||
'activityBar',
|
||||
'infiniteProgress'
|
||||
);
|
||||
utils.overlay.showMenu();
|
||||
utils.overlay.oncancel = function() {
|
||||
cancelled = true;
|
||||
importer ? importer.finish() : Contacts.hideOverlay();
|
||||
};
|
||||
var wakeLock = navigator.requestWakeLock('cpu');
|
||||
|
||||
var importedContacts = 0;
|
||||
// Delay for showing feedback to the user after importing
|
||||
var DELAY_FEEDBACK = 200;
|
||||
|
||||
utils.sdcard.retrieveFiles([
|
||||
'text/vcard',
|
||||
'text/x-vcard',
|
||||
'text/directory;profile=vCard',
|
||||
'text/directory'
|
||||
], ['vcf', 'vcard'], function(err, fileArray) {
|
||||
if (err) {
|
||||
return import_error(err, cb);
|
||||
}
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileArray.length) {
|
||||
var promises = [];
|
||||
fileArray.forEach(file => {
|
||||
promises.push(utils.sdcard.getTextFromFile(file, onContacts));
|
||||
});
|
||||
Promise.all(promises).then(results => {
|
||||
var numDupsMerged = results.reduce((sum, current) => {
|
||||
return sum + current;
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
utils.misc.setTimestamp('sd', () => {
|
||||
// Once the timestamp is saved, update the list
|
||||
updateTimestamps();
|
||||
checkNoContacts();
|
||||
resetWait(wakeLock);
|
||||
|
||||
if (!cancelled) {
|
||||
var msg1 = {
|
||||
id: 'memoryCardContacts-imported3',
|
||||
args: {
|
||||
n: importedContacts
|
||||
}
|
||||
};
|
||||
var msg2 = !numDupsMerged ? null : {
|
||||
id: 'contactsMerged',
|
||||
args: {
|
||||
numDups: numDupsMerged
|
||||
}
|
||||
};
|
||||
|
||||
utils.status.show(msg1, msg2);
|
||||
|
||||
logImportUsage('sd');
|
||||
|
||||
if (typeof cb === 'function') {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, DELAY_FEEDBACK);
|
||||
}).catch(error => {
|
||||
import_error(error);
|
||||
});
|
||||
} else {
|
||||
import_error('No contacts were found.', cb);
|
||||
}
|
||||
});
|
||||
|
||||
function onContacts(text) {
|
||||
if (cancelled) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
importer = new VCFReader(text);
|
||||
if (!text || !importer) {
|
||||
var error = 'No contacts were found';
|
||||
import_error(error);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
importer.onimported = imported_contact;
|
||||
importer.onerror = error => {
|
||||
import_error(error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
importer.process((unused, numDupsMerged) => {
|
||||
if (cancelled) {
|
||||
reject('Cancelled');
|
||||
Contacts.hideOverlay();
|
||||
return;
|
||||
}
|
||||
resolve(numDupsMerged);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function imported_contact() {
|
||||
importedContacts++;
|
||||
progress.update();
|
||||
}
|
||||
|
||||
function import_error(e, cb) {
|
||||
var cancel = {
|
||||
title: 'cancel',
|
||||
callback: function() {
|
||||
ConfirmDialog.hide();
|
||||
}
|
||||
};
|
||||
|
||||
var retry = {
|
||||
title: 'retry',
|
||||
isRecommend: true,
|
||||
callback: function() {
|
||||
ConfirmDialog.hide();
|
||||
// And now the action is reproduced one more time
|
||||
window.setTimeout(requireOverlay.bind(this, onSdImport), 0);
|
||||
}
|
||||
};
|
||||
ConfirmDialog.show(null, 'memoryCardContacts-error', cancel,
|
||||
retry);
|
||||
resetWait(wakeLock);
|
||||
if (typeof cb === 'function') {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Dismiss settings window and execute operations if values got modified
|
||||
var close = function close() {
|
||||
if (newOrderByLastName != null &&
|
||||
newOrderByLastName != orderByLastName && contacts.List) {
|
||||
contacts.List.setOrderByLastName(newOrderByLastName);
|
||||
utils.cookie.update({order: newOrderByLastName});
|
||||
Cache.evict();
|
||||
// Force the reset of the dom, we know that we changed the order
|
||||
contacts.List.load(null, true);
|
||||
orderByLastName = newOrderByLastName;
|
||||
}
|
||||
|
||||
Contacts.goBack();
|
||||
};
|
||||
|
||||
var checkOnline = function() {
|
||||
// Perform pending automatic logouts
|
||||
window.setTimeout(automaticLogout, 0);
|
||||
|
||||
// Other import services settings
|
||||
updateOptionStatus(importGmailOption, !navigator.onLine, true);
|
||||
updateOptionStatus(importLiveOption, !navigator.onLine, true);
|
||||
};
|
||||
|
||||
var checkNoContacts = function checkNoContacts() {
|
||||
var exportButton = exportContacts.firstElementChild;
|
||||
|
||||
ContactsService.isEmpty(function(error, isEmpty) {
|
||||
if (error) {
|
||||
window.console.warn(
|
||||
'Error while trying to know the contact number',
|
||||
error
|
||||
);
|
||||
// In case of error is safer to leave enabled
|
||||
exportButton.removeAttribute('disabled');
|
||||
bulkDeleteButton.removeAttribute('disabled');
|
||||
return;
|
||||
}
|
||||
if (isEmpty) {
|
||||
exportButton.setAttribute('disabled', 'disabled');
|
||||
bulkDeleteButton.setAttribute('disabled', 'disabled');
|
||||
setICEButton.setAttribute('disabled', 'disabled');
|
||||
} else {
|
||||
exportButton.removeAttribute('disabled');
|
||||
bulkDeleteButton.removeAttribute('disabled');
|
||||
setICEButton.removeAttribute('disabled');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function saveStatus(data) {
|
||||
window.asyncStorage.setItem(PENDING_LOGOUT_KEY, data);
|
||||
}
|
||||
|
||||
function automaticLogout() {
|
||||
if (navigator.offLine === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
LazyLoader.load(['/shared/js/contacts/utilities/http_rest.js'],
|
||||
function() {
|
||||
window.asyncStorage.getItem(PENDING_LOGOUT_KEY, function(data) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
var services = Object.keys(data);
|
||||
var numResponses = 0;
|
||||
|
||||
services.forEach(function(service) {
|
||||
var url = data[service];
|
||||
|
||||
var callbacks = {
|
||||
success: function logout_success() {
|
||||
numResponses++;
|
||||
window.console.log('Successfully logged out: ', service);
|
||||
delete data[service];
|
||||
if (numResponses === services.length) {
|
||||
saveStatus(data);
|
||||
}
|
||||
},
|
||||
error: function logout_error() {
|
||||
numResponses++;
|
||||
if (numResponses === services.length) {
|
||||
saveStatus(data);
|
||||
}
|
||||
},
|
||||
timeout: function logout_timeout() {
|
||||
numResponses++;
|
||||
if (numResponses === services.length) {
|
||||
saveStatus(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
Rest.get(url, callbacks);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var updateTimestamps = function updateTimestamps() {
|
||||
// TODO Add the same functionality to 'EXPORT' methods when ready.
|
||||
var importSources =
|
||||
document.querySelectorAll('#import-options li[data-source]');
|
||||
Array.prototype.forEach.call(importSources, function(node) {
|
||||
utils.misc.getTimestamp(node.dataset.source,
|
||||
function(time) {
|
||||
var spanID = 'notImported';
|
||||
if (time) {
|
||||
spanID = 'imported';
|
||||
var timeElement = node.querySelector('p > time');
|
||||
timeElement.setAttribute('datetime',
|
||||
(new Date(time)).toLocaleString());
|
||||
timeElement.textContent = utils.time.pretty(time);
|
||||
}
|
||||
node.querySelector('p > span').setAttribute('data-l10n-id', spanID);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var refresh = function refresh() {
|
||||
getData();
|
||||
checkOnline();
|
||||
checkSIMCard();
|
||||
utils.sdcard.getStatus(function statusUpdated() {
|
||||
updateStorageOptions(utils.sdcard.checkStorageCard());
|
||||
});
|
||||
updateTimestamps();
|
||||
checkNoContacts();
|
||||
};
|
||||
|
||||
return {
|
||||
'init': init,
|
||||
'close': close,
|
||||
'refresh': refresh,
|
||||
'cardStateChanged': checkSIMCard,
|
||||
'updateTimestamps': updateTimestamps,
|
||||
'showICEScreen' : showICEScreen,
|
||||
get navigation() { return navigationHandler; },
|
||||
'importFromSDCard': onSdImport,
|
||||
'importFromSIMCard': onSimImport
|
||||
};
|
||||
})();
|
||||
248
gaia/apps/communications/contacts/js/webrtc-client/webrtc_client.js
Executable file
248
gaia/apps/communications/contacts/js/webrtc-client/webrtc_client.js
Executable file
|
|
@ -0,0 +1,248 @@
|
|||
/* exports WebrtcClient */
|
||||
/* global MozActivity */
|
||||
/* global Promise */
|
||||
|
||||
/*
|
||||
* This class is in charge of adding, in a 'plug&play' way, the buttons
|
||||
* related with the new feature for calling through WebRTC other phone or
|
||||
* email, taking into account the Contact info.
|
||||
*
|
||||
* We will check with MozActivity if the app is installed or not, and if it
|
||||
* is installed we will show the buttons in the Detail of the contact.
|
||||
*/
|
||||
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var WEBRTC_CLIENTNAME = 'Firefox Hello';
|
||||
|
||||
var _webrtcClientIntegrationDOM, _detailListDOM;
|
||||
|
||||
var _cachedContact = null;
|
||||
|
||||
var _isInDetail = false;
|
||||
var _isWebrtcClientInstalled;
|
||||
var voiceCallButton, videoCallButton;
|
||||
|
||||
function _launchActivity(videoEnabled) {
|
||||
var activity = new MozActivity({
|
||||
name: 'webrtc-call',
|
||||
data: {
|
||||
video: videoEnabled,
|
||||
type: 'webcontacts/contact',
|
||||
contact: _cachedContact
|
||||
}
|
||||
});
|
||||
|
||||
activity.onerror = function() {
|
||||
// TODO Add string if needed
|
||||
};
|
||||
}
|
||||
|
||||
function _checkIfInstalled(forceCheck) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
// If the value was previously checked we dont need to recheck again.
|
||||
if (_isWebrtcClientInstalled !== undefined && !forceCheck) {
|
||||
_isWebrtcClientInstalled ? resolve() : reject();
|
||||
return;
|
||||
}
|
||||
var activity =
|
||||
new MozActivity(
|
||||
{
|
||||
name: 'webrtc-call',
|
||||
data: {
|
||||
type: 'webcontacts/contact',
|
||||
video: false,
|
||||
contact: null
|
||||
},
|
||||
// With this param we retrieve a list of apps
|
||||
// which could handle this activity.
|
||||
getFilterResults: true
|
||||
}
|
||||
);
|
||||
|
||||
activity.onsuccess = function(e) {
|
||||
if (activity.result.length > 0) {
|
||||
_isWebrtcClientInstalled = true;
|
||||
resolve();
|
||||
} else {
|
||||
_isWebrtcClientInstalled = false;
|
||||
reject();
|
||||
}
|
||||
};
|
||||
activity.onerror = function(e) {
|
||||
_isWebrtcClientInstalled = false;
|
||||
reject();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function _createDOM() {
|
||||
// Let's build the structure we are going to use
|
||||
// First of all the element is going to be added to a list
|
||||
// so we create the 'li' element
|
||||
_webrtcClientIntegrationDOM = document.createElement('li');
|
||||
_webrtcClientIntegrationDOM.id = 'webrtc-client-actions';
|
||||
|
||||
var title = document.createElement('h2');
|
||||
// This has been hardcoded to prevent uplifting issues to v2.0
|
||||
title.textContent = WEBRTC_CLIENTNAME;
|
||||
|
||||
var colsWrapper = document.createElement('div');
|
||||
colsWrapper.className = 'fillflow-twocols';
|
||||
|
||||
voiceCallButton = document.createElement('button');
|
||||
voiceCallButton.className = 'activity icon icon-webrtc-voice';
|
||||
voiceCallButton.setAttribute('data-l10n-id', 'audio');
|
||||
voiceCallButton.addEventListener(
|
||||
'click',
|
||||
function onVoiceCallRequest() {
|
||||
_launchActivity(false);
|
||||
}
|
||||
);
|
||||
|
||||
videoCallButton = document.createElement('button');
|
||||
videoCallButton.className = 'activity icon icon-webrtc-video';
|
||||
videoCallButton.setAttribute('data-l10n-id', 'video');
|
||||
videoCallButton.addEventListener(
|
||||
'click',
|
||||
function onVideoCallRequest() {
|
||||
_launchActivity(true);
|
||||
}
|
||||
);
|
||||
|
||||
colsWrapper.appendChild(voiceCallButton);
|
||||
colsWrapper.appendChild(videoCallButton);
|
||||
// Append elements to the new section
|
||||
_webrtcClientIntegrationDOM.appendChild(title);
|
||||
_webrtcClientIntegrationDOM.appendChild(colsWrapper);
|
||||
}
|
||||
|
||||
function _init() {
|
||||
// Cache the parent element
|
||||
_detailListDOM = document.getElementById('details-list');
|
||||
// Create the DOM for the new element in the 'details-list'
|
||||
_createDOM();
|
||||
}
|
||||
|
||||
function _insert() {
|
||||
// Get the section which is going to be shown after, we will use it
|
||||
// as a reference
|
||||
var reference;
|
||||
// Is there any phone element?
|
||||
var phoneNumbers = _detailListDOM.querySelectorAll('[data-phone]');
|
||||
var phonesLength = phoneNumbers && phoneNumbers.length;
|
||||
if (phonesLength) {
|
||||
reference = phoneNumbers[phonesLength - 1].nextSibling;
|
||||
}
|
||||
else {
|
||||
var emails = _detailListDOM.querySelectorAll('[data-mail]');
|
||||
var emailsLength = emails && emails.length;
|
||||
if (emailsLength) {
|
||||
reference = emails[emailsLength - 1];
|
||||
}
|
||||
}
|
||||
|
||||
_detailListDOM.insertBefore(
|
||||
_webrtcClientIntegrationDOM,
|
||||
reference
|
||||
);
|
||||
}
|
||||
|
||||
function _remove() {
|
||||
if (!!document.getElementById('webrtc-client-actions')) {
|
||||
_detailListDOM.removeChild(_webrtcClientIntegrationDOM);
|
||||
}
|
||||
}
|
||||
|
||||
function _render() {
|
||||
if (!_webrtcClientIntegrationDOM) {
|
||||
_init();
|
||||
}
|
||||
_insert();
|
||||
}
|
||||
|
||||
function _onVisibilityChange() {
|
||||
if (!document.hidden) {
|
||||
_checkIfInstalled(true).then(
|
||||
_onWebrtcClientInstalled,
|
||||
_onWebrtcClientUninstalled
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function _startListeningAppChanges() {
|
||||
document.addEventListener('visibilitychange', _onVisibilityChange);
|
||||
}
|
||||
|
||||
function _stopListeningAppChanges() {
|
||||
document.removeEventListener('visibilitychange', _onVisibilityChange);
|
||||
}
|
||||
|
||||
function _webRtcClientAvailable(contact) {
|
||||
var mailPresent = false;
|
||||
var telPresent = false;
|
||||
|
||||
if (Array.isArray(contact.email)) {
|
||||
if (contact.email.length > 0 && contact.email[0] &&
|
||||
contact.email[0].value && contact.email[0].value.trim()) {
|
||||
mailPresent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(contact.tel)) {
|
||||
if (contact.tel.length > 0 && contact.tel[0] &&
|
||||
contact.tel[0].value && contact.tel[0].value.trim()) {
|
||||
telPresent = true;
|
||||
}
|
||||
}
|
||||
|
||||
return telPresent || mailPresent;
|
||||
}
|
||||
|
||||
function _onWebrtcClientInstalled() {
|
||||
// If we are not in the detail of a contact, we
|
||||
// dont need to render any button
|
||||
if (!_webRtcClientAvailable(_cachedContact) || !_isInDetail) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If is not appended yet, we added it
|
||||
if (!document.getElementById('webrtc-client-actions')) {
|
||||
_render();
|
||||
}
|
||||
}
|
||||
|
||||
function _onWebrtcClientUninstalled() {
|
||||
_remove();
|
||||
}
|
||||
|
||||
var WebrtcClient = {
|
||||
start: function start(contact, forceCheck) {
|
||||
if (!_isInDetail) {
|
||||
// Specify that we are in the detail of a contact
|
||||
_isInDetail = true;
|
||||
// Listening for app changes
|
||||
_startListeningAppChanges();
|
||||
}
|
||||
// Cache the contact for future events
|
||||
_cachedContact = contact;
|
||||
// If installed we need to render
|
||||
_checkIfInstalled(forceCheck).then(
|
||||
_onWebrtcClientInstalled,
|
||||
_onWebrtcClientUninstalled
|
||||
);
|
||||
|
||||
},
|
||||
stop: function stop() {
|
||||
// Clean the previous params
|
||||
_cachedContact = null;
|
||||
_isInDetail = false;
|
||||
// Stop listening for app changes
|
||||
_stopListeningAppChanges();
|
||||
}
|
||||
};
|
||||
|
||||
exports.WebrtcClient = WebrtcClient;
|
||||
}(this));
|
||||
Loading…
Reference in a new issue