VulpesOS/gaia/apps/communications/contacts/js/fb/fb_link.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

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