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:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View 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();
}

View 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;
}
};

View 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
};
})();

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

View 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;
})();

View 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;
}
};
})();

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

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

View 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
};
})();

View 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
};
})();

View 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
};
};

View 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
};
};

View 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
};
};

View 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
};
};

View 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;
};

View 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);
});
};
})();
}

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

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

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

View 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; }
};
})();

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

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

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

View file

@ -0,0 +1,7 @@
/* global navigationStack */
/* exported MainNavigation */
(function(exports) {
'use strict';
exports.MainNavigation = new navigationStack('view-contacts-list');
}(window));

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

View 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
};
})();

View 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 : '';
};
}

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

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

View 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')}
]
};

View 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;
})();

View 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;
};
}

View 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
};
}();

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

View 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
};
}());

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

View 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;
};
})();
}

View 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;
})();

View 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]');
};

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

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

View 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
};
})();

File diff suppressed because it is too large Load diff

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

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

File diff suppressed because it is too large Load diff

View 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
};
})();

View 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
};
})();

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