Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
677 lines
19 KiB
JavaScript
Executable file
677 lines
19 KiB
JavaScript
Executable file
'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);
|
|
}
|